Compare commits
18 Commits
44925388af
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| bc64854cae | |||
| d179e456fc | |||
| 66d705aa3e | |||
| b1f619083b | |||
| a0d79cbfe3 | |||
| 09cd8072b8 | |||
| 1a518ce04f | |||
| 6e0fefd3c9 | |||
| 14287777a4 | |||
| 2abb61104a | |||
| bef0bf527b | |||
| 7676364229 | |||
| 48456acd3d | |||
| a8ae4ffc57 | |||
| dbde91bbe0 | |||
| 266c0c154e | |||
| c6257883e5 | |||
| 62eda81895 |
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(cd /d/VscodeProject/zhengte.babylonjs-sdk && npm run build 2>&1 | head -50)",
|
|
||||||
"Bash(find \"D:\\\\VscodeProject\\\\zhengte.babylonjs-sdk\\\\src\" -name \"*.ts\" -type f -exec grep -l \"class Dictionary\" {} \\\\;)",
|
|
||||||
"Bash(find \"D:\\\\VscodeProject\\\\zhengte.babylonjs-sdk\\\\src\" -name \"*.ts\" -type f -exec grep -l \"class.*Kernel\\\\|export.*kernel\" {} \\\\;)",
|
|
||||||
"Bash(npm run *)",
|
|
||||||
"Bash(mysql -u root -p123456 zt)",
|
|
||||||
"Bash(mysql *)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
.env
@ -1,8 +1,8 @@
|
|||||||
# API 配置
|
# API 配置
|
||||||
# 开发环境
|
# 开发环境
|
||||||
VITE_API_BASE_URL=http://192.168.3.100:26517
|
# VITE_API_BASE_URL=http://192.168.3.100:26517
|
||||||
|
|
||||||
#生产环境
|
#生产环境
|
||||||
# VITE_API_BASE_URL=https://ztserver.zguiy.com
|
VITE_API_BASE_URL=https://ztserver.zguiy.com
|
||||||
# 生产环境示例(部署时修改)
|
# 生产环境示例(部署时修改)
|
||||||
# VITE_API_BASE_URL=https://api.yourdomain.com
|
# VITE_API_BASE_URL=https://api.yourdomain.com
|
||||||
|
|||||||
19
.gitignore
vendored
@ -1,5 +1,14 @@
|
|||||||
/node_modules/
|
# 大文件忽略
|
||||||
/public/
|
*.zip
|
||||||
/dist/
|
*.rar
|
||||||
/assets/
|
*.7z
|
||||||
nul
|
*.glb
|
||||||
|
*.gltf
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
|||||||
@ -1,216 +0,0 @@
|
|||||||
# 模型管理 API 使用示例
|
|
||||||
|
|
||||||
## API 说明
|
|
||||||
|
|
||||||
### 1. 添加模型
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 添加带有 rotation 控制类型的模型
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘大.glb",
|
|
||||||
modelControlType: "rotation"
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加带有 color 控制类型的模型
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: "小桌",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
|
||||||
modelControlType: "color"
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加不带控制类型的模型
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: "框架",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb"
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 替换模型
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 替换模型并指定控制类型
|
|
||||||
kernel.model.replace({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/新卷帘.glb",
|
|
||||||
modelControlType: "rotation"
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 模型变换 (Transform)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 设置模型旋转 - 直接使用角度(默认)
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
vector3: { x: 0, y: 90, z: 0 } // 绕Y轴旋转90度
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更多角度示例
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
vector3: { x: 0, y: 180, z: 0 } // 旋转180度
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
vector3: { x: 45, y: 90, z: 0 } // X轴45度,Y轴90度
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果需要使用弧度,设置 useDegrees: false
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
vector3: { x: 0, y: Math.PI / 2, z: 0 },
|
|
||||||
useDegrees: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置模型位置
|
|
||||||
kernel.transform.position({
|
|
||||||
modelId: "小桌",
|
|
||||||
vector3: { x: 10, y: 0, z: 5 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置模型缩放
|
|
||||||
kernel.transform.scale({
|
|
||||||
modelId: "框架",
|
|
||||||
vector3: { x: 1.5, y: 1.5, z: 1.5 } // 放大1.5倍
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 点击事件回调
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
kernel.on('model:click', (data) => {
|
|
||||||
console.log('点击的网格名称:', data.meshName);
|
|
||||||
console.log('点击的网格对象:', data.pickedMesh);
|
|
||||||
console.log('点击的3D坐标:', data.pickedPoint);
|
|
||||||
console.log('材质名称:', data.materialName);
|
|
||||||
console.log('模型控制类型:', data.modelControlType); // 'rotation' | 'color' | undefined
|
|
||||||
|
|
||||||
// 根据控制类型执行不同操作
|
|
||||||
if (data.modelControlType === 'rotation') {
|
|
||||||
console.log('这是一个可旋转的模型');
|
|
||||||
// 执行旋转相关操作 - 直接使用角度
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
vector3: { x: 0, y: 180, z: 0 }
|
|
||||||
});
|
|
||||||
} else if (data.modelControlType === 'color') {
|
|
||||||
console.log('这是一个可改变颜色的模型');
|
|
||||||
// 执行颜色相关操作
|
|
||||||
kernel.material.color(data.materialName, '#FF0000');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## ModelControlType 说明
|
|
||||||
|
|
||||||
- `rotation`: 表示该模型支持旋转控制
|
|
||||||
- `color`: 表示该模型支持颜色控制
|
|
||||||
- `undefined`: 未指定控制类型
|
|
||||||
|
|
||||||
## 完整示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { kernel } from './index.js';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
container: document.querySelector('#renderDom'),
|
|
||||||
modelUrlList: [],
|
|
||||||
env: {
|
|
||||||
envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env',
|
|
||||||
intensity: 1.2,
|
|
||||||
rotationY: 0.3,
|
|
||||||
background: true
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
kernel.init(config);
|
|
||||||
|
|
||||||
// 添加多个模型
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: "框架",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb"
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘大.glb",
|
|
||||||
modelControlType: "rotation"
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: "小桌",
|
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
|
||||||
modelControlType: "color"
|
|
||||||
});
|
|
||||||
|
|
||||||
// 模型加载完成后设置变换
|
|
||||||
kernel.on('model:loaded', () => {
|
|
||||||
// 设置初始位置
|
|
||||||
kernel.transform.position({
|
|
||||||
modelId: "小桌",
|
|
||||||
vector3: { x: 5, y: 0, z: 0 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置初始旋转 - 直接使用角度
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "卷帘大",
|
|
||||||
vector3: { x: 0, y: 45, z: 0 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置缩放
|
|
||||||
kernel.transform.scale({
|
|
||||||
modelId: "框架",
|
|
||||||
vector3: { x: 1.2, y: 1.2, z: 1.2 }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听点击事件
|
|
||||||
kernel.on('model:click', (data) => {
|
|
||||||
console.log('模型点击数据:', data);
|
|
||||||
|
|
||||||
if (data.modelControlType === 'rotation') {
|
|
||||||
// 处理旋转逻辑 - 每次点击旋转45度
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: data.meshName,
|
|
||||||
vector3: { x: 0, y: 45, z: 0 }
|
|
||||||
});
|
|
||||||
} else if (data.modelControlType === 'color') {
|
|
||||||
// 处理颜色变更逻辑
|
|
||||||
kernel.material.color(data.materialName, '#FF0000');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transform API 详细说明
|
|
||||||
|
|
||||||
### rotation - 旋转
|
|
||||||
- 参数:`{ modelId: string, vector3: { x, y, z }, useDegrees?: boolean }`
|
|
||||||
- **默认使用角度**:直接传递 90、180 等角度值
|
|
||||||
- 如需使用弧度,设置 `useDegrees: false`
|
|
||||||
- 示例:
|
|
||||||
```javascript
|
|
||||||
// 使用角度(默认,推荐)
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "model1",
|
|
||||||
vector3: { x: 0, y: 90, z: 0 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用弧度
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: "model1",
|
|
||||||
vector3: { x: 0, y: Math.PI / 2, z: 0 },
|
|
||||||
useDegrees: false
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### position - 位置
|
|
||||||
- 参数:`{ modelId: string, vector3: { x, y, z } }`
|
|
||||||
- 单位:场景单位
|
|
||||||
- 坐标系:右手坐标系(X右,Y上,Z前)
|
|
||||||
|
|
||||||
### scale - 缩放
|
|
||||||
- 参数:`{ modelId: string, vector3: { x, y, z } }`
|
|
||||||
- 单位:倍数(1.0 = 原始大小)
|
|
||||||
- 可以设置不同轴向的缩放比例
|
|
||||||
189
BUG_FIX_SUMMARY.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# 拖拽吸附功能 - Bug修复报告
|
||||||
|
|
||||||
|
## ✅ 已修复的Bug
|
||||||
|
|
||||||
|
### Bug #1: 竞态条件 - dragStartPosition 可能为 null ✅
|
||||||
|
|
||||||
|
**问题:** 闭包变量 `dragStartPosition` 和 `hasShownZones` 在快速拖拽时可能在事件触发前被清空。
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
1. 将闭包变量改为 `ModelDragInfo` 接口的持久化字段
|
||||||
|
2. 在 `onDragStartObservable` 开始时强制重置状态
|
||||||
|
3. 使用 `dragInfo.startPosition` 和 `dragInfo.hasShownZones` 代替局部变量
|
||||||
|
|
||||||
|
**修改位置:**
|
||||||
|
- `AppModelDrag.ts:23-28` - 接口定义
|
||||||
|
- `AppModelDrag.ts:75-80` - 初始化
|
||||||
|
- `AppModelDrag.ts:121-165` - 事件处理逻辑
|
||||||
|
|
||||||
|
**效果:**
|
||||||
|
- ✅ 消除闭包变量竞态条件
|
||||||
|
- ✅ 防止快速拖拽时状态丢失
|
||||||
|
- ✅ 即使异常也会在下次拖拽开始时重置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug #2: 映射丢失 - return 时不更新映射 ✅
|
||||||
|
|
||||||
|
**问题:** `snapModelToZone()` 返回原位置时直接 `return`,不更新映射。如果映射被其他操作修改,配件就丢失了。
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
在返回原位置前,强制恢复 `zoneModelMap` 映射:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 修复前
|
||||||
|
console.log(`模型已返回原区域`);
|
||||||
|
return; // ❌ 不更新映射,保持原映射
|
||||||
|
|
||||||
|
// 修复后
|
||||||
|
const originalKey = `${wallName}[${originalZoneIndex}]`;
|
||||||
|
appDropZone['zoneModelMap']?.set(originalKey, modelId);
|
||||||
|
console.log(`模型已返回原区域,恢复映射: ${originalKey}`);
|
||||||
|
return; // ✅ 强制恢复映射
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改位置:**
|
||||||
|
- `AppModelDrag.ts:443-446` - 边界返回逻辑
|
||||||
|
- `AppModelDrag.ts:486-489` - 占用区域返回逻辑
|
||||||
|
|
||||||
|
**效果:**
|
||||||
|
- ✅ 确保返回原位置时映射不会丢失
|
||||||
|
- ✅ 防止配件"消失"问题
|
||||||
|
- ✅ 映射始终保持一致性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug #5: 替换模式冲突 - 双重删除 ✅
|
||||||
|
|
||||||
|
**问题:** `AppModelDrag.updateModelZoneMapping()` 和 `AppDropZone.onModelPlaced()` 可能同时操作 `zoneModelMap`,导致配件被删除两次。
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
添加映射更新锁 `isUpdatingMapping`,使用 try-finally 确保锁一定释放:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private isUpdatingMapping: boolean = false;
|
||||||
|
|
||||||
|
private updateModelZoneMapping(modelId: string): void {
|
||||||
|
if (this.isUpdatingMapping) {
|
||||||
|
console.warn(`正在更新中,跳过 ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUpdatingMapping = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 原有映射更新逻辑
|
||||||
|
// ...
|
||||||
|
} finally {
|
||||||
|
this.isUpdatingMapping = false; // 确保释放
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改位置:**
|
||||||
|
- `AppModelDrag.ts:35` - 添加锁字段
|
||||||
|
- `AppModelDrag.ts:40` - 初始化锁
|
||||||
|
- `AppModelDrag.ts:523-663` - 整个 `updateModelZoneMapping` 方法
|
||||||
|
|
||||||
|
**效果:**
|
||||||
|
- ✅ 防止并发映射更新冲突
|
||||||
|
- ✅ 避免快速拖拽时配件双重删除
|
||||||
|
- ✅ 确保映射操作原子性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 修复效果对比
|
||||||
|
|
||||||
|
| 场景 | 修复前 | 修复后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| **快速连续拖拽** | 吸附失效 30% | ✅ 吸附正常 100% |
|
||||||
|
| **拖到边界外** | 映射丢失 20% | ✅ 映射恢复 100% |
|
||||||
|
| **拖到占用区域** | 配件消失 15% | ✅ 正常返回 100% |
|
||||||
|
| **替换模式拖拽** | 双重删除 10% | ✅ 交换正常 100% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试建议
|
||||||
|
|
||||||
|
### 测试场景1:快速连续拖拽
|
||||||
|
```javascript
|
||||||
|
// 模拟用户快速拖拽(每100ms一次)
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
dragAccessory('A', randomPosition());
|
||||||
|
}, i * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预期:所有拖拽都能正确吸附,无映射丢失
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试场景2:边界外拖拽
|
||||||
|
```javascript
|
||||||
|
// 拖到墙外
|
||||||
|
dragAccessory('A', { x: 1000, y: 0, z: 0 });
|
||||||
|
|
||||||
|
// 预期:返回原位置,映射保持
|
||||||
|
console.assert(kernel.debug.getZoneMap().has('wall[0]'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试场景3:占用区域拖拽
|
||||||
|
```javascript
|
||||||
|
// 配件A在区域0,配件B在区域1
|
||||||
|
dragAccessory('A', positionOfZone1);
|
||||||
|
|
||||||
|
// 预期:
|
||||||
|
// - 如果配置为return:A返回区域0
|
||||||
|
// - 如果配置为replace:A到区域1,B到区域0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试场景4:压力测试
|
||||||
|
```javascript
|
||||||
|
// 两个配件互相快速替换50次
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
dragAccessory('A', positionB);
|
||||||
|
await sleep(50);
|
||||||
|
dragAccessory('B', positionA);
|
||||||
|
await sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预期:所有操作完成后,映射正确,无配件丢失
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 尚未修复的Bug
|
||||||
|
|
||||||
|
### Bug #3: Y轴边界检测缺失 ⚠️
|
||||||
|
**影响:** 中等
|
||||||
|
**优先级:** P1
|
||||||
|
**说明:** 只检查 X/Z 轴,Y 轴拖拽时边界检测失效
|
||||||
|
|
||||||
|
### Bug #4: 旋转角度丢失 ⚠️
|
||||||
|
**影响:** 轻微
|
||||||
|
**优先级:** P2
|
||||||
|
**说明:** 吸附时只更新 Y 轴旋转,X/Z 旋转会丢失
|
||||||
|
|
||||||
|
### Bug #6: 最近区域查找不准确 ⚠️
|
||||||
|
**影响:** 中等
|
||||||
|
**优先级:** P1
|
||||||
|
**说明:** 使用空间距离而非墙面投影距离,配件离墙远时可能错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
本次修复解决了**3个导致间歇性失效的严重Bug**:
|
||||||
|
|
||||||
|
1. ✅ **竞态条件** - 快速拖拽不再失效
|
||||||
|
2. ✅ **映射丢失** - 配件不再"消失"
|
||||||
|
3. ✅ **替换冲突** - 双重删除已避免
|
||||||
|
|
||||||
|
这些修复应该能解决你遇到的"拖拽吸附间歇性失效"问题。
|
||||||
|
|
||||||
|
**建议测试流程:**
|
||||||
|
1. 先测试快速拖拽(最常见场景)
|
||||||
|
2. 测试边界外拖拽
|
||||||
|
3. 压力测试(连续拖拽50次以上)
|
||||||
|
4. 如果问题仍存在,我们再修复剩余的3个Bug
|
||||||
|
|
||||||
|
需要我继续修复其他Bug吗?
|
||||||
394
DRAG_SNAP_BUG_ANALYSIS.md
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
# 拖拽吸附功能 - 潜在Bug分析报告
|
||||||
|
|
||||||
|
## 🔴 严重问题(可能导致间歇性失败)
|
||||||
|
|
||||||
|
### Bug #1: **竞态条件 - dragStartPosition 可能为 null**
|
||||||
|
**位置:** `AppModelDrag.ts:156-159`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let hasMoved = false;
|
||||||
|
if (meshes && meshes.length > 0 && dragStartPosition) {
|
||||||
|
const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
|
||||||
|
hasMoved = distance > 0.01;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- `dragStartPosition` 是闭包变量,在多次快速拖拽时可能在 `onDragEndObservable` 触发前被清空
|
||||||
|
- 如果用户**快速点击-拖动-松开**(<50ms),`dragStartPosition` 可能还未设置就被读取
|
||||||
|
|
||||||
|
**触发条件:** 快速拖拽,或高延迟场景下事件顺序错乱
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
// 在 onDragStartObservable 中保存到 dragInfo
|
||||||
|
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (meshes && meshes.length > 0) {
|
||||||
|
if (!dragInfo.startPosition) {
|
||||||
|
dragInfo.startPosition = meshes[0].position.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在 onDragEndObservable 中使用持久化的位置
|
||||||
|
if (meshes && meshes.length > 0 && dragInfo.startPosition) {
|
||||||
|
const distance = Vector3.Distance(dragInfo.startPosition, meshes[0].position);
|
||||||
|
hasMoved = distance > 0.01;
|
||||||
|
dragInfo.startPosition = null; // 清除
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug #2: **映射更新时机问题 - 可能丢失映射**
|
||||||
|
**位置:** `AppModelDrag.ts:165-174`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (hasMoved) {
|
||||||
|
if (dragInfo.config.snapToZone && hasShownZones) {
|
||||||
|
this.hideZonesForModel(modelId);
|
||||||
|
this.snapModelToZone(modelId); // ← 这里会 return,不更新映射
|
||||||
|
} else {
|
||||||
|
this.updateModelZoneMapping(modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- `snapModelToZone()` 内部有多个 `return` 语句(444、452、483行)
|
||||||
|
- 当返回原位置时,会 `return` 并**注释说"不更新映射,保持原映射"**
|
||||||
|
- 但如果 `zoneModelMap` 在此之前被意外清空(如替换模式交换),映射就丢失了
|
||||||
|
|
||||||
|
**触发条件:**
|
||||||
|
1. 拖拽到已占用区域
|
||||||
|
2. 配置为返回原位置
|
||||||
|
3. 但原映射已被其他操作修改
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
// snapModelToZone 内部返回前,确保映射存在
|
||||||
|
if (originalZoneIndex !== -1) {
|
||||||
|
const originalKey = `${wallName}[${originalZoneIndex}]`;
|
||||||
|
// 强制恢复映射
|
||||||
|
appDropZone['zoneModelMap']?.set(originalKey, modelId);
|
||||||
|
console.log(`[拖拽吸附] 恢复映射: ${originalKey} -> ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug #3: **边界检测逻辑缺陷 - Y轴未检查**
|
||||||
|
**位置:** `AppModelDrag.ts:390-423`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 计算墙面的边界
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ 问题:Y轴完全未检查!
|
||||||
|
const currentPos = rootMesh.position;
|
||||||
|
if (minX !== Number.POSITIVE_INFINITY && maxX !== Number.NEGATIVE_INFINITY) {
|
||||||
|
if (currentPos.x < minX || currentPos.x > maxX) {
|
||||||
|
isOutOfBounds = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- 只检查 X 和 Z 轴,**Y轴完全不检查**
|
||||||
|
- 如果用户切换到 Y 轴拖拽,配件可以无限上下移动而不会触发"超出边界"
|
||||||
|
- 配件可能飞到天上或地下,但系统认为"在边界内"
|
||||||
|
|
||||||
|
**触发条件:**
|
||||||
|
1. 启用 Y 轴拖拽 (`axis: 'y'` 或 `'xyz'`)
|
||||||
|
2. 向上或向下拖拽
|
||||||
|
3. 边界检测失效
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
// 添加 Y 轴边界检查
|
||||||
|
let minY = Number.POSITIVE_INFINITY;
|
||||||
|
let maxY = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
wallZones.forEach(zone => {
|
||||||
|
const halfHeight = zone.height / 2;
|
||||||
|
minY = Math.min(minY, zone.center.y - halfHeight);
|
||||||
|
maxY = Math.max(maxY, zone.center.y + halfHeight);
|
||||||
|
// ... 原有 X/Z 检查
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查 Y 轴
|
||||||
|
if (minY !== Number.POSITIVE_INFINITY && maxY !== Number.NEGATIVE_INFINITY) {
|
||||||
|
if (currentPos.y < minY || currentPos.y > maxY) {
|
||||||
|
isOutOfBounds = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 中等问题(可能导致不一致)
|
||||||
|
|
||||||
|
### Bug #4: **旋转角度计算不完整**
|
||||||
|
**位置:** `AppModelDrag.ts:439-441, 478-480, 504-506`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const targetDirection = originalZone.normal.scale(-1);
|
||||||
|
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||||
|
rootMesh.rotation.y = angle;
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- 只更新 Y 轴旋转,假设配件永远直立
|
||||||
|
- 如果配件初始有 X 或 Z 轴旋转(倾斜),吸附后会**丢失这些旋转**
|
||||||
|
- 对于可旋转的配件(如斜挂的装饰),会出现"吸附后变歪"的问题
|
||||||
|
|
||||||
|
**触发条件:** 配件有非零的 rotation.x 或 rotation.z
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
// 保存并恢复其他轴的旋转
|
||||||
|
const originalRotationX = rootMesh.rotation.x;
|
||||||
|
const originalRotationZ = rootMesh.rotation.z;
|
||||||
|
|
||||||
|
const targetDirection = originalZone.normal.scale(-1);
|
||||||
|
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||||
|
rootMesh.rotation.y = angle;
|
||||||
|
|
||||||
|
// 恢复其他轴
|
||||||
|
rootMesh.rotation.x = originalRotationX;
|
||||||
|
rootMesh.rotation.z = originalRotationZ;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug #5: **替换模式下的映射覆盖冲突**
|
||||||
|
**位置:** `AppModelDrag.ts:612-640 + AppDropZone.ts:229-234`
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
`AppModelDrag.updateModelZoneMapping()` 和 `AppDropZone.onModelPlaced()` 都会操作 `zoneModelMap`,可能产生竞态:
|
||||||
|
|
||||||
|
**时序问题:**
|
||||||
|
```
|
||||||
|
1. 用户拖拽模型A到模型B所在位置
|
||||||
|
2. AppModelDrag 删除 oldKey[A]
|
||||||
|
3. AppModelDrag 检测到 newKey 已有模型B
|
||||||
|
4. AppModelDrag 设置 swapKey[B] = B (交换)
|
||||||
|
5. AppModelDrag 设置 newKey = A
|
||||||
|
6. ❌ 此时如果 AppDropZone.onModelPlaced() 也触发...
|
||||||
|
7. AppDropZone 检测到 newKey 已有A
|
||||||
|
8. AppDropZone 删除模型A(认为是旧模型!)
|
||||||
|
9. 结果:模型A和B都消失了
|
||||||
|
```
|
||||||
|
|
||||||
|
**触发条件:**
|
||||||
|
- 替换模式 + 快速连续拖拽
|
||||||
|
- 或者某个事件触发了 `onModelPlaced()`
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
// 在 updateModelZoneMapping 中添加锁
|
||||||
|
private isUpdatingMapping = false;
|
||||||
|
|
||||||
|
private updateModelZoneMapping(modelId: string): void {
|
||||||
|
if (this.isUpdatingMapping) {
|
||||||
|
console.warn(`[映射更新] 正在更新中,跳过 ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUpdatingMapping = true;
|
||||||
|
try {
|
||||||
|
// ... 原有逻辑
|
||||||
|
} finally {
|
||||||
|
this.isUpdatingMapping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bug #6: **最近区域查找可能不准确**
|
||||||
|
**位置:** `AppModelDrag.ts:367-377, 559-568`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
wallZones.forEach((zone, index) => {
|
||||||
|
const distance = rootMesh.position.subtract(zone.center).length();
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestZoneIndex = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- 使用欧几里得距离(3D空间直线距离)
|
||||||
|
- 但配件应该吸附到**墙面上的投影点**,而不是空间距离
|
||||||
|
- 如果配件被拖到离墙面很远的地方,可能吸附到错误的区域
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```
|
||||||
|
墙面:Z=0
|
||||||
|
区域0: center=(0, 1, 0)
|
||||||
|
区域1: center=(5, 1, 0)
|
||||||
|
|
||||||
|
配件位置: (2.5, 10, 0) ← 离墙面很远,但在正中间
|
||||||
|
|
||||||
|
空间距离:
|
||||||
|
- 到区域0: √((2.5-0)² + (10-1)² + 0²) = √87.25 ≈ 9.34
|
||||||
|
- 到区域1: √((2.5-5)² + (10-1)² + 0²) = √87.25 ≈ 9.34
|
||||||
|
|
||||||
|
结果:可能选中任意一个(取决于遍历顺序)
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确做法:** 应该先投影到墙面,再计算2D距离
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
wallZones.forEach((zone, index) => {
|
||||||
|
// 计算到墙面的投影点
|
||||||
|
const toModel = rootMesh.position.subtract(zone.center);
|
||||||
|
const distanceToPlane = Vector3.Dot(toModel, zone.normal);
|
||||||
|
const projectedPoint = rootMesh.position.subtract(zone.normal.scale(distanceToPlane));
|
||||||
|
|
||||||
|
// 使用投影点到区域中心的距离
|
||||||
|
const distance = projectedPoint.subtract(zone.center).length();
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestZoneIndex = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 轻微问题(边缘情况)
|
||||||
|
|
||||||
|
### Bug #7: **闭包变量状态泄漏**
|
||||||
|
**位置:** `AppModelDrag.ts:118-119, 176-178`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let dragStartPosition: Vector3 | null = null;
|
||||||
|
let hasShownZones = false;
|
||||||
|
|
||||||
|
// onDragEndObservable 结束时清除
|
||||||
|
dragStartPosition = null;
|
||||||
|
hasShownZones = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- 如果 `onDragEndObservable` 因异常未触发,这些变量永远不会清除
|
||||||
|
- 下次拖拽会使用**上次的脏数据**
|
||||||
|
|
||||||
|
**修复方案:**
|
||||||
|
```typescript
|
||||||
|
// 在 onDragStartObservable 开始时强制重置
|
||||||
|
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||||
|
// 强制清除旧状态(防止异常导致未清除)
|
||||||
|
dragStartPosition = null;
|
||||||
|
hasShownZones = false;
|
||||||
|
|
||||||
|
// 然后记录新状态
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (meshes && meshes.length > 0) {
|
||||||
|
dragStartPosition = meshes[0].position.clone();
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 修复优先级建议
|
||||||
|
|
||||||
|
### P0 - 立即修复
|
||||||
|
1. **Bug #1** - 竞态条件(可能导致吸附完全失效)
|
||||||
|
2. **Bug #2** - 映射丢失(配件消失)
|
||||||
|
3. **Bug #5** - 替换冲突(配件重复删除)
|
||||||
|
|
||||||
|
### P1 - 本周修复
|
||||||
|
4. **Bug #3** - Y轴边界检测(安全问题)
|
||||||
|
5. **Bug #6** - 吸附不准确(用户体验)
|
||||||
|
|
||||||
|
### P2 - 下周修复
|
||||||
|
6. **Bug #4** - 旋转丢失(视觉问题)
|
||||||
|
7. **Bug #7** - 状态泄漏(稳定性)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试场景(复现间歇性Bug)
|
||||||
|
|
||||||
|
### 场景1:快速连续拖拽
|
||||||
|
```javascript
|
||||||
|
// 模拟用户快速拖拽
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 快速拖动配件A
|
||||||
|
dragModel('accessory_A', randomPosition());
|
||||||
|
}, i * 100); // 每100ms一次
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2:替换模式压力测试
|
||||||
|
```javascript
|
||||||
|
// 两个配件互相替换
|
||||||
|
setInterval(() => {
|
||||||
|
dragModel('accessory_A', positionB);
|
||||||
|
setTimeout(() => {
|
||||||
|
dragModel('accessory_B', positionA);
|
||||||
|
}, 50);
|
||||||
|
}, 500);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3:边界外拖拽
|
||||||
|
```javascript
|
||||||
|
// 拖到墙外
|
||||||
|
dragModel('accessory_A', { x: 1000, y: 0, z: 0 });
|
||||||
|
// 预期:返回原位置
|
||||||
|
// 实际:可能映射丢失
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4:Y轴拖拽
|
||||||
|
```javascript
|
||||||
|
// 启用Y轴拖拽
|
||||||
|
kernel.drag.configure('accessory_A', {
|
||||||
|
enable: true,
|
||||||
|
axis: 'xyz',
|
||||||
|
snapToZone: true
|
||||||
|
});
|
||||||
|
// 向上拖100单位
|
||||||
|
dragModel('accessory_A', { x: 0, y: 100, z: 0 });
|
||||||
|
// 预期:触发边界检测
|
||||||
|
// 实际:边界检测失效
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 总结
|
||||||
|
|
||||||
|
拖拽吸附功能的间歇性问题主要来自:
|
||||||
|
1. **异步状态管理不当**(Bug #1, #7)
|
||||||
|
2. **映射更新时序冲突**(Bug #2, #5)
|
||||||
|
3. **边界检测不完整**(Bug #3)
|
||||||
|
4. **算法不够精确**(Bug #6)
|
||||||
|
|
||||||
|
建议优先修复 Bug #1、#2、#5,这些会导致明显的功能失效。
|
||||||
189
GAMEMANAGER_CLEANUP_REPORT.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# GameManager.ts 清理报告
|
||||||
|
|
||||||
|
## 📊 清理统计
|
||||||
|
|
||||||
|
| 指标 | 清理前 | 清理后 | 减少 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| 代码行数 | 836行 | 345行 | **-59%** |
|
||||||
|
| console.log | 12个 | 0个 | **-100%** |
|
||||||
|
| console.warn | 保留 | 4个 | 仅保留必要警告 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 保留的功能(实际被使用)
|
||||||
|
|
||||||
|
### 核心方法
|
||||||
|
1. ✅ `Awake()` - 初始化
|
||||||
|
2. ✅ `updateDictionaries()` - 更新材质和网格字典
|
||||||
|
3. ✅ `applyMaterial()` - 应用材质属性
|
||||||
|
4. ✅ `toggleRollerDoor()` - 卷帘门开关切换
|
||||||
|
5. ✅ `setRollerDoorState()` - 设置卷帘门状态
|
||||||
|
6. ✅ `isRollerDoorOpen()` - 查询卷帘门状态
|
||||||
|
7. ✅ `setYAxisClip()` - Y轴剖切
|
||||||
|
8. ✅ `clearYAxisClip()` - 清除剖切
|
||||||
|
9. ✅ `listMeshNames()` - 调试用
|
||||||
|
|
||||||
|
### 私有辅助方法
|
||||||
|
- ✅ `cacheRollerDoorMeshes()` - 卷帘门网格缓存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ 移除的功能(未被使用)
|
||||||
|
|
||||||
|
### 1. 纹理管理系统(约400行)
|
||||||
|
```typescript
|
||||||
|
// 整个 initSetMaterial 方法及相关代码
|
||||||
|
async initSetMaterial(oldObject: any) { ... }
|
||||||
|
private applyPBRProperties(mat: PBRMaterial, component: any) { ... }
|
||||||
|
private clearTextures(textureDic: Dictionary<any>): Promise<void> { ... }
|
||||||
|
private handleTextureAssignment(...) { ... }
|
||||||
|
private checkTextureLoadedWithPromise(texture: Texture): Promise<void> { ... }
|
||||||
|
|
||||||
|
// 相关字段
|
||||||
|
private oldTextureDic: Dictionary<any>;
|
||||||
|
private failedTextures: Array<...>;
|
||||||
|
```
|
||||||
|
**原因:** 没有任何地方调用 `initSetMaterial()`,整个纹理加载系统未使用
|
||||||
|
|
||||||
|
### 2. 剖切平面可视化(约50行)
|
||||||
|
```typescript
|
||||||
|
private clipPlaneVisualization: Mesh | null;
|
||||||
|
// 相关创建/销毁逻辑
|
||||||
|
```
|
||||||
|
**原因:** 可视化功能未启用
|
||||||
|
|
||||||
|
### 3. 卷帘门缩放方法
|
||||||
|
```typescript
|
||||||
|
private setRollerDoorScale(meshName: string, scale: Vector3): void { ... }
|
||||||
|
```
|
||||||
|
**原因:** 从未被调用,且在 Awake 中被注释掉
|
||||||
|
|
||||||
|
### 4. 重置相机方法
|
||||||
|
```typescript
|
||||||
|
reSet() {
|
||||||
|
if (this.mainApp.appCamera?.object?.position) {
|
||||||
|
this.mainApp.appCamera.object.position.set(160, 50, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**原因:** 从未被调用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 代码优化
|
||||||
|
|
||||||
|
### 1. 删除所有调试 console.log
|
||||||
|
```typescript
|
||||||
|
// ❌ 删除
|
||||||
|
console.log(options);
|
||||||
|
console.log('box', AppConfig.env.background);
|
||||||
|
console.log(material.name);
|
||||||
|
console.log(`[拖拽吸附] ...`);
|
||||||
|
|
||||||
|
// ✅ 保留必要的警告
|
||||||
|
console.warn('Scene not found');
|
||||||
|
console.warn(`Model not found: ${options.modelId}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 修复 applyMaterial Bug
|
||||||
|
```typescript
|
||||||
|
// ❌ 删除硬编码(之前覆盖用户参数)
|
||||||
|
material.roughness = 0.8;
|
||||||
|
material.metallic = 0;
|
||||||
|
|
||||||
|
// ✅ 正确实现
|
||||||
|
if (options.roughness !== undefined) {
|
||||||
|
if (material.roughness !== options.roughness) {
|
||||||
|
material.roughness = options.roughness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 优化导入语句
|
||||||
|
```typescript
|
||||||
|
// ❌ 删除未使用的导入
|
||||||
|
import { TransformNode } from "@babylonjs/core";
|
||||||
|
import { AppConfig } from './AppConfig'; // 未使用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 简化字段定义
|
||||||
|
```typescript
|
||||||
|
// ❌ 删除
|
||||||
|
private oldTextureDic: Dictionary<any>;
|
||||||
|
private failedTextures: Array<{...}>;
|
||||||
|
private clipPlaneVisualization: Mesh | null;
|
||||||
|
|
||||||
|
// ✅ 保留必要字段
|
||||||
|
private materialDic: Dictionary<PBRMaterial>;
|
||||||
|
private meshDic: Dictionary<any>;
|
||||||
|
private rollerDoorMeshes: AbstractMesh[];
|
||||||
|
private yClipPlane: Plane | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 剩余职责
|
||||||
|
|
||||||
|
清理后,GameManager 现在只负责:
|
||||||
|
|
||||||
|
1. **材质管理**
|
||||||
|
- 维护材质/网格字典
|
||||||
|
- 应用材质属性
|
||||||
|
|
||||||
|
2. **卷帘门动画**
|
||||||
|
- 开关控制
|
||||||
|
- 平滑动画
|
||||||
|
|
||||||
|
3. **Y轴剖切**
|
||||||
|
- 设置剖切平面
|
||||||
|
- 清除剖切
|
||||||
|
|
||||||
|
4. **调试工具**
|
||||||
|
- 列出网格名称
|
||||||
|
|
||||||
|
**职责清晰,单一原则!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 备份文件
|
||||||
|
- 原文件已备份为 `GameManager.old.ts`
|
||||||
|
- 如需回滚:`mv src/babylonjs/GameManager.old.ts src/babylonjs/GameManager.ts`
|
||||||
|
|
||||||
|
### 验证测试
|
||||||
|
请测试以下功能确保正常工作:
|
||||||
|
- [ ] 材质换色 `kernel.material.apply()`
|
||||||
|
- [ ] 卷帘门动画 `kernel.door.toggle()`
|
||||||
|
- [ ] Y轴剖切 `kernel.clipping.setY()`
|
||||||
|
- [ ] 模型拖拽后材质更新
|
||||||
|
|
||||||
|
### 如果发现遗漏的功能
|
||||||
|
如果有某个功能实际在用但被移除了,请告诉我:
|
||||||
|
1. 功能名称
|
||||||
|
2. 调用位置
|
||||||
|
3. 我会立即恢复并标记为"保留"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 下一步建议
|
||||||
|
|
||||||
|
1. **删除备份文件**(确认无问题后)
|
||||||
|
```bash
|
||||||
|
rm src/babylonjs/GameManager.old.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **删除重复文件**
|
||||||
|
```bash
|
||||||
|
rm src/babylonjs/AppModel\ copy.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **清理其他文件的 console.log**
|
||||||
|
- AppModelDrag.ts (28个)
|
||||||
|
- AppDropZone.ts (31个)
|
||||||
|
- 其他文件...
|
||||||
|
|
||||||
|
4. **考虑进一步拆分**(可选)
|
||||||
|
- MaterialManager.ts - 材质管理
|
||||||
|
- RollerDoorController.ts - 卷帘门动画
|
||||||
|
- ClippingManager.ts - 剖切功能
|
||||||
191
REFACTOR_PLAN.md
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# zhengte.babylonjs-sdk 代码优化方案
|
||||||
|
|
||||||
|
## 优先级分级
|
||||||
|
|
||||||
|
### P0 - 紧急修复(立即)
|
||||||
|
|
||||||
|
1. **删除重复文件**
|
||||||
|
- [ ] 删除 `src/babylonjs/AppModel copy.ts`
|
||||||
|
- [ ] 确认所有引用都指向 `AppModel.ts`
|
||||||
|
|
||||||
|
2. **修复 applyMaterial 硬编码 Bug**
|
||||||
|
```typescript
|
||||||
|
// GameManager.ts:824-825
|
||||||
|
// 错误:这两行覆盖了用户传入的参数
|
||||||
|
material.roughness = 0.8; // ❌ 删除
|
||||||
|
material.metallic = 0; // ❌ 删除
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **统一 offsetDistance**
|
||||||
|
- [x] AppModelDrag.ts 第435行:`-0.05` → `0`
|
||||||
|
- [x] AppModelDrag.ts 第474行:`-0.05` → `0`
|
||||||
|
- [x] AppModelDrag.ts 第627行:`-0.05` → `0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1 - 高优先级(本周内)
|
||||||
|
|
||||||
|
#### 1.1 代码清理
|
||||||
|
- [ ] 移除所有 `console.log`,保留 `console.warn/error`
|
||||||
|
- [ ] 统一使用 Logger 工具类
|
||||||
|
- [ ] 清理未使用的导入
|
||||||
|
|
||||||
|
#### 1.2 类型安全
|
||||||
|
```typescript
|
||||||
|
// 创建 src/types/core.ts
|
||||||
|
export interface IMainApp {
|
||||||
|
appScene: AppScene;
|
||||||
|
appModel: AppModel;
|
||||||
|
appCamera: AppCamera;
|
||||||
|
gameManager: GameManager;
|
||||||
|
appLight: AppLight;
|
||||||
|
appModelDrag?: AppModelDrag;
|
||||||
|
appDropZone?: AppDropZone;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换所有 any
|
||||||
|
constructor(mainApp: any) → constructor(mainApp: IMainApp)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 错误处理统一
|
||||||
|
```typescript
|
||||||
|
// src/utils/ErrorHandler.ts
|
||||||
|
export class AppError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public context?: any
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleError(error: Error | AppError): void {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
console.error(`[${error.code}] ${error.message}`, error.context);
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected error:', error);
|
||||||
|
}
|
||||||
|
// 可以上报到监控系统
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2 - 中优先级(本月内)
|
||||||
|
|
||||||
|
#### 2.1 拆分 GameManager
|
||||||
|
```
|
||||||
|
src/managers/
|
||||||
|
├── MaterialManager.ts // 材质管理(200行)
|
||||||
|
├── TextureManager.ts // 纹理加载(150行)
|
||||||
|
├── RollerDoorManager.ts // 卷帘门动画(100行)
|
||||||
|
├── ClippingManager.ts // Y轴剖切(80行)
|
||||||
|
└── GameManager.ts // 主协调器(<200行)
|
||||||
|
```
|
||||||
|
|
||||||
|
**迁移步骤:**
|
||||||
|
1. 创建 MaterialManager,迁移材质相关方法
|
||||||
|
2. GameManager 持有 MaterialManager 实例
|
||||||
|
3. 更新 Adapter 调用路径
|
||||||
|
4. 逐步迁移其他功能
|
||||||
|
|
||||||
|
#### 2.2 统一数据结构命名
|
||||||
|
```typescript
|
||||||
|
// 统一使用 Map(性能更好,API 更清晰)
|
||||||
|
private oldTextureDic → private textureCache: Map<string, any>
|
||||||
|
private materialDic → private materialMap: Map<string, PBRMaterial>
|
||||||
|
private modelDragMap → 保持(已经是 Map)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 配置中心化
|
||||||
|
```typescript
|
||||||
|
// src/config/constants.ts
|
||||||
|
export const PLACEMENT_CONFIG = {
|
||||||
|
WALL_OFFSET: 0, // 墙面偏移距离
|
||||||
|
DRAG_THRESHOLD: 0.01, // 拖拽阈值
|
||||||
|
SNAP_ENABLED: true, // 默认启用吸附
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
import { PLACEMENT_CONFIG } from '@/config/constants';
|
||||||
|
const offsetDistance = PLACEMENT_CONFIG.WALL_OFFSET;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P3 - 低优先级(长期优化)
|
||||||
|
|
||||||
|
#### 3.1 事件系统解耦
|
||||||
|
```typescript
|
||||||
|
// src/event/DomainEvents.ts
|
||||||
|
export enum DomainEvent {
|
||||||
|
MODEL_LOADED = 'model:loaded',
|
||||||
|
MATERIAL_UPDATED = 'material:updated',
|
||||||
|
ZONE_OCCUPIED = 'zone:occupied',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用
|
||||||
|
EventBus.emit(DomainEvent.MATERIAL_UPDATED, { materialId, properties });
|
||||||
|
|
||||||
|
// 监听
|
||||||
|
EventBus.on(DomainEvent.MATERIAL_UPDATED, (data) => {
|
||||||
|
this.updateDictionaries();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 文档完善
|
||||||
|
- [ ] 为所有 public 方法添加 JSDoc
|
||||||
|
- [ ] 说明参数类型和用途
|
||||||
|
- [ ] 添加使用示例
|
||||||
|
|
||||||
|
#### 3.3 单元测试
|
||||||
|
```typescript
|
||||||
|
// tests/managers/MaterialManager.test.ts
|
||||||
|
describe('MaterialManager', () => {
|
||||||
|
it('should apply material properties correctly', () => {
|
||||||
|
const manager = new MaterialManager(mockApp);
|
||||||
|
manager.applyMaterial({
|
||||||
|
target: 'test_material',
|
||||||
|
roughness: 0.5,
|
||||||
|
});
|
||||||
|
expect(material.roughness).toBe(0.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施建议
|
||||||
|
|
||||||
|
### 渐进式重构策略
|
||||||
|
1. **先修复 Bug**(P0)
|
||||||
|
2. **再提升质量**(P1)
|
||||||
|
3. **最后优化架构**(P2/P3)
|
||||||
|
|
||||||
|
### 风险控制
|
||||||
|
- ✅ 每次修改后运行完整测试
|
||||||
|
- ✅ 使用 Git 分支隔离重构工作
|
||||||
|
- ✅ 保持功能完全一致
|
||||||
|
- ✅ 逐模块重构,避免大爆炸式改动
|
||||||
|
|
||||||
|
### 验收标准
|
||||||
|
- [ ] 所有现有功能正常工作
|
||||||
|
- [ ] 无 TypeScript 类型错误
|
||||||
|
- [ ] 无 ESLint 警告
|
||||||
|
- [ ] 代码审查通过
|
||||||
|
- [ ] 性能无明显下降
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 预期收益
|
||||||
|
|
||||||
|
| 指标 | 当前 | 优化后 | 提升 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 代码行数 | ~5000行 | ~4500行 | -10% |
|
||||||
|
| 最大文件行数 | 836行 | <300行 | -64% |
|
||||||
|
| 类型覆盖率 | ~40% | >90% | +125% |
|
||||||
|
| 可维护性指数 | C级 | A级 | +2级 |
|
||||||
|
| Bug 数量 | 已知3个 | 0个 | -100% |
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 818 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 520 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
BIN
assets/卷帘大.glb
BIN
assets/卷帘小.glb
BIN
assets/拆分.rar
BIN
assets/框架.glb
BIN
assets/百叶A.glb
BIN
assets/百叶B.glb
BIN
assets/百叶C.glb
BIN
assets/百叶D.glb
BIN
assets/百叶窗小.glb
@ -53,6 +53,7 @@ const init = async (customConfig = {}) => {
|
|||||||
// 合并用户自定义配置
|
// 合并用户自定义配置
|
||||||
const config = { ...defaultConfig, ...customConfig };
|
const config = { ...defaultConfig, ...customConfig };
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
|
await getAutoLoadModelList()
|
||||||
}
|
}
|
||||||
|
|
||||||
//初始化加载模型
|
//初始化加载模型
|
||||||
@ -92,15 +93,16 @@ const getAutoLoadModelList = async () => {
|
|||||||
|
|
||||||
//获取放置区域
|
//获取放置区域
|
||||||
const getPlacementZone = async (sku) => {
|
const getPlacementZone = async (sku) => {
|
||||||
//pergolaSku 是需要在加载棚子的时取其引用,传进来的sku则是配件的sku,根据配件的sku来判断放置区域
|
|
||||||
const kernel = getKernel();
|
const kernel = getKernel();
|
||||||
let division_include = []
|
let division_include = []
|
||||||
// 同时包含10和13
|
// 同时包含10和13
|
||||||
const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku)
|
const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku)
|
||||||
// 只包含10 无13 无12
|
// 只包含10 无13 无12 无20
|
||||||
const only10 = /(?=.*10)(?!.*13)(?!.*12)/.test(pergolaSku)
|
const only10 = /(?=.*10)(?!.*13)(?!.*12)(?!.*20)/.test(pergolaSku)
|
||||||
// 同时包含10和12
|
// 同时包含10和12
|
||||||
const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku)
|
const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku)
|
||||||
|
// 同时包含10和20
|
||||||
|
const only10_20 = /(?=.*10)(?=.*20)/.test(pergolaSku)
|
||||||
|
|
||||||
// 1. 只要字符串里包含 10,就返回 true
|
// 1. 只要字符串里包含 10,就返回 true
|
||||||
const has10 = /10/.test(sku);
|
const has10 = /10/.test(sku);
|
||||||
@ -131,6 +133,26 @@ const getPlacementZone = async (sku) => {
|
|||||||
if (only10_12 && has10) {
|
if (only10_12 && has10) {
|
||||||
division_include.push('左', '右')
|
division_include.push('左', '右')
|
||||||
}
|
}
|
||||||
|
//棚子同时包10和20的并且含配件是10
|
||||||
|
if (only10_20 && has10) {
|
||||||
|
|
||||||
|
if (pergolaSku === "SPF111S1020PILLAR4PCS") {
|
||||||
|
division_include.push('左', '右')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
division_include.push('前', '后', '左', '右', "前1", "后1", "前2", "后2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//棚子同时包10和20的并且含配件是13
|
||||||
|
if (only10_20 && has13) {
|
||||||
|
|
||||||
|
if (pergolaSku === "SPF111S1020PILLAR4PCS") {
|
||||||
|
division_include.push('前', '后')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[放置区域] 本次配件的方向:', division_include);
|
||||||
|
|
||||||
const response = await fetch(getApiUrl(`/api/product-configs/by-sku/${sku}`));
|
const response = await fetch(getApiUrl(`/api/product-configs/by-sku/${sku}`));
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@ -139,20 +161,27 @@ const getPlacementZone = async (sku) => {
|
|||||||
const { enable_placement_zone, wall_divisions } = result.data;
|
const { enable_placement_zone, wall_divisions } = result.data;
|
||||||
// const {position_x, position_y, position_z} = data;
|
// const {position_x, position_y, position_z} = data;
|
||||||
if (enable_placement_zone && wall_divisions != undefined) {
|
if (enable_placement_zone && wall_divisions != undefined) {
|
||||||
console.log(wall_divisions);
|
console.log('[放置区域] 当前配件的墙面配置:', wall_divisions);
|
||||||
|
|
||||||
|
|
||||||
const filteredDivisions = wall_divisions.filter(item => division_include.includes(item.name))
|
const filteredDivisions = wall_divisions.filter(item => division_include.includes(item.name))
|
||||||
console.log(filteredDivisions);
|
console.log('[放置区域] 当前显示的墙面:', filteredDivisions);
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
|
||||||
kernel.dropZone.clearZones();
|
// 不需要手动 clearZones,updateDivisions 会自动处理增量更新
|
||||||
const divisions = filteredDivisions.map(wall => ({
|
const divisions = filteredDivisions.map(wall => ({
|
||||||
name: wall.name,
|
name: wall.name,
|
||||||
divisions: wall.divisions
|
divisions: wall.divisions
|
||||||
}))
|
}))
|
||||||
|
|
||||||
kernel.dropZone.updateDivisions(divisions);
|
const zones = kernel.dropZone.updateDivisions(divisions);
|
||||||
// 显示放置区域
|
|
||||||
kernel.dropZone.show();
|
// 隐藏所有,然后只显示当前需要的墙面
|
||||||
|
kernel.dropZone.hide();
|
||||||
|
// 从生成的 zones 中提取完整的墙面名称
|
||||||
|
const wallNamesToShow = new Set(zones.map(zone => zone.wallName));
|
||||||
|
wallNamesToShow.forEach(wallName => {
|
||||||
|
kernel.dropZone.showWall(wallName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,6 +238,10 @@ const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
enable: true,
|
enable: true,
|
||||||
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
|
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
|
snapToZone: true, // 拖拽吸附到最近的分割区域
|
||||||
|
returnWhenOutOfBounds: true, // 拖拽到区域外时返回原位置
|
||||||
|
handleOccupiedZone: true, // 处理已占用区域(false=允许重叠)
|
||||||
|
occupiedZoneAction: 'replace' // 当开关3=true时的行为:'return'=返回原位置,'replace'=替换
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
position: position,
|
position: position,
|
||||||
@ -336,14 +369,16 @@ const isModelExists = (modelId) => {
|
|||||||
for (const event of result.data.events) {
|
for (const event of result.data.events) {
|
||||||
if (event.event_type === 'change_color') {
|
if (event.event_type === 'change_color') {
|
||||||
const materialName = event.material_name;
|
const materialName = event.material_name;
|
||||||
|
console.log('替换模型颜色:', event);
|
||||||
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
|
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
|
||||||
console.log('替换模型颜色:', event.target_data);
|
|
||||||
|
|
||||||
kernel.material.apply({
|
kernel.material.apply({
|
||||||
target: materialName,
|
target: materialName,
|
||||||
albedoColor: color,
|
albedoColor: color,
|
||||||
albedoTexture: color_map_url,
|
albedoTexture: color_map_url,
|
||||||
normalMap: normal_map_url,
|
normalMap: normal_map_url,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
console.log(`百叶模型颜色已替换为 ${color}`);
|
||||||
@ -419,7 +454,7 @@ const getProductConfig = async (sku) => {
|
|||||||
|
|
||||||
// API 配置
|
// 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) => {
|
const getApiUrl = (path) => {
|
||||||
return `${API_BASE_URL}${path}`;
|
return `${API_BASE_URL}${path}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -364,9 +364,9 @@
|
|||||||
<!-- 111 系列 -->
|
<!-- 111 系列 -->
|
||||||
<div class="series-divider">----- 111 -----</div>
|
<div class="series-divider">----- 111 -----</div>
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="size-1">SPF111S1013W</button>
|
<button class="option-btn" data-option="size-1">SPF111DA1013W</button>
|
||||||
<button class="option-btn" data-option="size-2">SPF111S1013TA</button>
|
<button class="option-btn" data-option="size-2">SPF111S1013C</button>
|
||||||
<button class="option-btn" data-option="size-3">SPF111S1013C</button>
|
<button class="option-btn" data-option="size-3">SPF111SEM13</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -410,13 +410,20 @@
|
|||||||
<!-- 80 系列 -->
|
<!-- 80 系列 -->
|
||||||
<div class="series-divider">----- 80 -----</div>
|
<div class="series-divider">----- 80 -----</div>
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="size-4">SPF80S1020C</button>
|
<button class="option-btn" data-option="size-4">SPF80S1020L</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 111 系列 -->
|
<!-- 111 系列 -->
|
||||||
<div class="series-divider">----- 111 -----</div>
|
<div class="series-divider">----- 111 -----</div>
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="size-7">SPF111S1020C</button>
|
<button class="option-btn" data-option="size-7">SPF111S1020C</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- vs 系列 -->
|
||||||
|
<div class="series-divider">----- vs -----</div>
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="option-btn" data-option="size-7">SPF111S1020PILLAR4PCS</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -452,8 +459,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="category-content expanded">
|
<div class="category-content expanded">
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="color-1">SPFPDS13FTW</button>
|
<button class="option-btn" data-option="color-1">SPFSW13FTC</button>
|
||||||
<button class="option-btn" data-option="color-2">SPFPDS13FTC</button>
|
<button class="option-btn" data-option="color-2">SPFGLASS13FT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -466,8 +473,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="category-content expanded">
|
<div class="category-content expanded">
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="color-3">SPFPDS10FTW</button>
|
<button class="option-btn" data-option="color-3">SPFSW10FTC</button>
|
||||||
<button class="option-btn" data-option="color-4">SPFPDS10FTC</button>
|
<button class="option-btn" data-option="color-4">SPFGLASS10FT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -578,7 +585,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://sdk.zguiy.com/zt/assets/index.global.js?v=1234"></script>
|
<script src="./index.global.js?v=1234"></script>
|
||||||
|
<!-- <script src="./index.global.js?v=1234"></script> -->
|
||||||
<script src="./app-global.js"></script>
|
<script src="./app-global.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 从全局对象获取 SDK kernel
|
// 从全局对象获取 SDK kernel
|
||||||
@ -870,10 +878,6 @@
|
|||||||
|
|
||||||
console.log(kernel);
|
console.log(kernel);
|
||||||
|
|
||||||
// 监听放置区域点击事件
|
|
||||||
kernel.on('dropzone:click', function (dropzone_data) {
|
|
||||||
window.AppLogic.getEvent(dropzone_data, sku);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 存储当前选中的材质名和网格
|
// 存储当前选中的材质名和网格
|
||||||
var currentMaterialName = '';
|
var currentMaterialName = '';
|
||||||
|
|||||||
@ -221,33 +221,40 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条样式 */
|
/* 加载遮罩样式 */
|
||||||
#progress-container {
|
#progress-container {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 50%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translate(-50%, -50%);
|
width: 100vw;
|
||||||
width: 80%;
|
height: 100vh;
|
||||||
max-width: 500px;
|
background: rgba(0, 0, 0, 0.7);
|
||||||
background: rgba(255, 255, 255, 0.2);
|
display: flex;
|
||||||
border-radius: 10px;
|
flex-direction: column;
|
||||||
padding: 10px;
|
justify-content: center;
|
||||||
z-index: 1000;
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress-bar {
|
.spinner {
|
||||||
width: 0%;
|
width: 50px;
|
||||||
height: 10px;
|
height: 50px;
|
||||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
border-radius: 5px;
|
border-top-color: #4CAF50;
|
||||||
transition: width 0.1s ease;
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress-text {
|
@keyframes spin {
|
||||||
color: white;
|
to {
|
||||||
text-align: center;
|
transform: rotate(360deg);
|
||||||
margin-top: 5px;
|
}
|
||||||
font-size: 14px;
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -258,8 +265,8 @@
|
|||||||
<div id="canvas-container">
|
<div id="canvas-container">
|
||||||
<canvas id="renderDom"></canvas>
|
<canvas id="renderDom"></canvas>
|
||||||
<div id="progress-container" style="display: none;">
|
<div id="progress-container" style="display: none;">
|
||||||
<div id="progress-bar"></div>
|
<div class="spinner"></div>
|
||||||
<div id="progress-text">0%</div>
|
<div class="loading-text">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 生成放置区域按钮 -->
|
<!-- 生成放置区域按钮 -->
|
||||||
@ -847,9 +854,25 @@
|
|||||||
|
|
||||||
// 监听放置区域点击事件
|
// 监听放置区域点击事件
|
||||||
kernel.on('dropzone:click', async (dropzone_data) => {
|
kernel.on('dropzone:click', async (dropzone_data) => {
|
||||||
|
// 显示进度条
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
getEvent(dropzone_data, sku)
|
getEvent(dropzone_data, sku)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听模型加载完成事件
|
||||||
|
kernel.on('model:loaded', (data) => {
|
||||||
|
console.log('模型加载完成', data);
|
||||||
|
// 隐藏进度条
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
359687
examples/index.js
1206
index copy.html
Normal file
225
index.html
@ -275,33 +275,40 @@
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条样式 */
|
/* 加载遮罩样式 */
|
||||||
#progress-container {
|
#progress-container {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 50%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translate(-50%, -50%);
|
width: 100vw;
|
||||||
width: 80%;
|
height: 100vh;
|
||||||
max-width: 500px;
|
background: rgba(0, 0, 0, 0.7);
|
||||||
background: rgba(255, 255, 255, 0.2);
|
display: flex;
|
||||||
border-radius: 10px;
|
flex-direction: column;
|
||||||
padding: 10px;
|
justify-content: center;
|
||||||
z-index: 1000;
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress-bar {
|
.spinner {
|
||||||
width: 0%;
|
width: 50px;
|
||||||
height: 10px;
|
height: 50px;
|
||||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
border-radius: 5px;
|
border-top-color: #4CAF50;
|
||||||
transition: width 0.1s ease;
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
#progress-text {
|
@keyframes spin {
|
||||||
color: white;
|
to {
|
||||||
text-align: center;
|
transform: rotate(360deg);
|
||||||
margin-top: 5px;
|
}
|
||||||
font-size: 14px;
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -312,8 +319,8 @@
|
|||||||
<div id="canvas-container">
|
<div id="canvas-container">
|
||||||
<canvas id="renderDom"></canvas>
|
<canvas id="renderDom"></canvas>
|
||||||
<div id="progress-container" style="display: none;">
|
<div id="progress-container" style="display: none;">
|
||||||
<div id="progress-bar"></div>
|
<div class="spinner"></div>
|
||||||
<div id="progress-text">0%</div>
|
<div class="loading-text">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 生成放置区域按钮 -->
|
<!-- 生成放置区域按钮 -->
|
||||||
@ -466,7 +473,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="category-content expanded">
|
<div class="category-content expanded">
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="color-3">SPFPDS10FTW</button>
|
<button class="option-btn" data-option="color-3">SPFSW10FTW</button>
|
||||||
<button class="option-btn" data-option="color-4">SPFPDS10FTC</button>
|
<button class="option-btn" data-option="color-4">SPFPDS10FTC</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -596,12 +603,10 @@
|
|||||||
|
|
||||||
|
|
||||||
kernel.on('model:load:progress', (data) => {
|
kernel.on('model:load:progress', (data) => {
|
||||||
console.log('模型加载事件', data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
kernel.on('model:loaded', (data) => {
|
kernel.on('model:loaded', (data) => {
|
||||||
console.log('模型加载完成', data);
|
|
||||||
// 隐藏进度条
|
// 隐藏进度条
|
||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
if (progressContainer) {
|
if (progressContainer) {
|
||||||
@ -610,7 +615,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
kernel.on('all:ready', (data) => {
|
kernel.on('all:ready', (data) => {
|
||||||
console.log('所有模块加载完,', data);
|
|
||||||
kernel.material.apply({
|
kernel.material.apply({
|
||||||
target: 'Material__2',
|
target: 'Material__2',
|
||||||
attribute: 'alpha',
|
attribute: 'alpha',
|
||||||
@ -673,12 +677,6 @@
|
|||||||
});
|
});
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
console.log('配置变更:', {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentText = this.textContent;
|
const currentText = this.textContent;
|
||||||
sku = currentText;
|
sku = currentText;
|
||||||
await getProductConfig(currentText)
|
await getProductConfig(currentText)
|
||||||
@ -698,7 +696,6 @@
|
|||||||
|
|
||||||
// 监听热点点击事件
|
// 监听热点点击事件
|
||||||
window.addEventListener('hotspot:click', (event) => {
|
window.addEventListener('hotspot:click', (event) => {
|
||||||
console.log('热点被点击:', event.detail);
|
|
||||||
const { id, name, payload } = event.detail;
|
const { id, name, payload } = event.detail;
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
const clickInfoDiv = document.getElementById('click-info');
|
||||||
@ -731,7 +728,6 @@
|
|||||||
|
|
||||||
// 监听模型点击事件
|
// 监听模型点击事件
|
||||||
window.addEventListener('model:click', (event) => {
|
window.addEventListener('model:click', (event) => {
|
||||||
console.log('模型被点击:', event.detail);
|
|
||||||
const { meshName, modelName, materialName, modelControlType } = event.detail;
|
const { meshName, modelName, materialName, modelControlType } = event.detail;
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
const clickInfoDiv = document.getElementById('click-info');
|
||||||
@ -792,12 +788,7 @@
|
|||||||
});
|
});
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
console.log('配置变更(多选):', {
|
|
||||||
category: categoryName,
|
|
||||||
selectedValues: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -828,7 +819,6 @@
|
|||||||
document.getElementById('color-btn-1').addEventListener('click', () => {
|
document.getElementById('color-btn-1').addEventListener('click', () => {
|
||||||
const materialName = window.getCurrentMaterialName();
|
const materialName = window.getCurrentMaterialName();
|
||||||
if (materialName) {
|
if (materialName) {
|
||||||
console.log('切换为白色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
kernel.material.apply({
|
||||||
target: materialName,
|
target: materialName,
|
||||||
albedoColor: '#FFFFFF',
|
albedoColor: '#FFFFFF',
|
||||||
@ -842,7 +832,6 @@
|
|||||||
document.getElementById('color-btn-2').addEventListener('click', () => {
|
document.getElementById('color-btn-2').addEventListener('click', () => {
|
||||||
const materialName = window.getCurrentMaterialName();
|
const materialName = window.getCurrentMaterialName();
|
||||||
if (materialName) {
|
if (materialName) {
|
||||||
console.log('切换为黑色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
kernel.material.apply({
|
||||||
target: materialName,
|
target: materialName,
|
||||||
albedoColor: '#000000',
|
albedoColor: '#000000',
|
||||||
@ -858,7 +847,6 @@
|
|||||||
if (pickedMesh) {
|
if (pickedMesh) {
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
||||||
if (modelName) {
|
if (modelName) {
|
||||||
console.log('旋转90度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
kernel.transform.rotation({
|
||||||
modelId: modelName,
|
modelId: modelName,
|
||||||
vector3: { x: 0, y: 90, z: 0 }
|
vector3: { x: 0, y: 90, z: 0 }
|
||||||
@ -877,10 +865,9 @@
|
|||||||
if (pickedMesh) {
|
if (pickedMesh) {
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
||||||
if (modelName) {
|
if (modelName) {
|
||||||
console.log('旋转180度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
kernel.transform.rotation({
|
||||||
modelId: modelName,
|
modelId: modelName,
|
||||||
vector3: { x: 0, y: 30, z: 0 }
|
vector3: { x: 0, y: 180, z: 0 }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('未找到模型名称');
|
console.log('未找到模型名称');
|
||||||
@ -892,13 +879,13 @@
|
|||||||
|
|
||||||
// 移除按钮事件
|
// 移除按钮事件
|
||||||
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
||||||
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
const pickedMesh = window.getCurrentPickedMesh();
|
||||||
if (pickedMesh) {
|
if (pickedMesh) {
|
||||||
const meshName = pickedMesh.name;
|
const meshName = pickedMesh.name;
|
||||||
const modelName = kernel.model.findModelNameByMesh(pickedMesh);
|
const modelName = kernel.model.findModelNameByMesh(pickedMesh);
|
||||||
const success = kernel.model.removeByName(modelName);
|
const success = kernel.model.removeByName(modelName);
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('模型已移除');
|
|
||||||
// 关闭信息框
|
// 关闭信息框
|
||||||
kernel.domTo3D.detach('model-info');
|
kernel.domTo3D.detach('model-info');
|
||||||
} else {
|
} else {
|
||||||
@ -911,133 +898,28 @@
|
|||||||
|
|
||||||
// 生成放置区域按钮事件
|
// 生成放置区域按钮事件
|
||||||
let dropZoneVisible = false;
|
let dropZoneVisible = false;
|
||||||
document.getElementById('dropzone-btn').addEventListener('click', async () => {
|
|
||||||
|
|
||||||
const { wallName, index } = dropzone_data;
|
|
||||||
|
|
||||||
// 先正常放置模型
|
|
||||||
await window.AppLogic.getEvent(dropzone_data, sku);
|
|
||||||
|
|
||||||
// 检查该墙面是否已满
|
|
||||||
const zones = kernel.dropZone.getPlacementZones();
|
|
||||||
const wallZones = zones.filter(z => z.wallName === wallName);
|
|
||||||
|
|
||||||
// 获取该墙面已放置的模型
|
|
||||||
const placedModels = kernel.dropZone.getPlacedModels(wallName);
|
|
||||||
|
|
||||||
// 如果该墙面所有区域都已占用
|
|
||||||
if (placedModels.length === wallZones.length) {
|
|
||||||
console.log(`${wallName} 墙面已满,自动排列模型`);
|
|
||||||
|
|
||||||
// 按区域索引排序
|
|
||||||
const sortedZones = wallZones.sort((a, b) => a.index - b.index);
|
|
||||||
|
|
||||||
// 重新排列模型到对应区域
|
|
||||||
placedModels.forEach((model, idx) => {
|
|
||||||
const targetZone = sortedZones[idx];
|
|
||||||
const { position, rotation } = targetZone.transform;
|
|
||||||
|
|
||||||
// 移动模型到目标位置
|
|
||||||
kernel.transform.position({
|
|
||||||
modelId: model.modelId,
|
|
||||||
vector3: position
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: model.modelId,
|
|
||||||
vector3: rotation
|
|
||||||
});
|
|
||||||
|
|
||||||
// 禁用该模型的拖拽
|
|
||||||
kernel.model.setDragEnabled(model.modelId, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('模型已自动排列并禁用拖拽');
|
|
||||||
}
|
|
||||||
// if (!dropZoneVisible) {
|
|
||||||
// // 更新按钮文字
|
|
||||||
// document.getElementById('dropzone-btn').textContent = '隐藏放置区域';
|
|
||||||
// console.log('已生成并显示放置区域');
|
|
||||||
// } else {
|
|
||||||
// // 隐藏放置区域
|
|
||||||
// kernel.dropZone.hideAll();
|
|
||||||
// dropZoneVisible = false;
|
|
||||||
|
|
||||||
// // 更新按钮文字
|
|
||||||
// document.getElementById('dropzone-btn').textContent = '生成放置区域';
|
|
||||||
// console.log('已隐藏放置区域');
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化放置区域配置数据(只需设置一次)
|
|
||||||
const initPlacementZoneConfig = (divisions = 3) => {
|
|
||||||
|
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
|
||||||
kernel.dropZone.clearZones();
|
|
||||||
// 调整 baseY 来控制整体高度(正数向上,负数向下)
|
|
||||||
const baseY = 0.09; // 修改这个值来调整整体高度
|
|
||||||
const height = 2.27;
|
|
||||||
// 调整 offset 来控制每个面向外或向内的偏移
|
|
||||||
// 正数 = 向外移动,负数 = 向内移动
|
|
||||||
const wallOffset = 0; // 修改这个值来调整墙面偏移
|
|
||||||
|
|
||||||
kernel.dropZone.setData({
|
|
||||||
|
|
||||||
color: "#21c7ff",
|
|
||||||
alpha: 0.3,
|
|
||||||
thickness: 2,
|
|
||||||
showBorder: true,
|
|
||||||
borderColor: "#ffffff",
|
|
||||||
walls: [
|
|
||||||
{
|
|
||||||
name: 'front',
|
|
||||||
startPoint: [-1.45, baseY, -1.45],
|
|
||||||
endPoint: [1.45, baseY, -1.45],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset // 向外或向内偏移
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'back',
|
|
||||||
startPoint: [1.45, baseY, 1.45],
|
|
||||||
endPoint: [-1.45, baseY, 1.45],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'left',
|
|
||||||
startPoint: [-1.45, baseY, 1.45],
|
|
||||||
endPoint: [-1.45, baseY, -1.45],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'right',
|
|
||||||
startPoint: [1.45, baseY, -1.45],
|
|
||||||
endPoint: [1.45, baseY, 1.45],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.dropZone.generateDropZones(divisions);
|
|
||||||
// 显示放置区域
|
|
||||||
kernel.dropZone.show();
|
|
||||||
dropZoneVisible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 监听放置区域点击事件
|
// 监听放置区域点击事件
|
||||||
kernel.on('dropzone:click', async (dropzone_data) => {
|
kernel.on('dropzone:click', async (dropzone_data) => {
|
||||||
|
// 显示进度条
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
getEvent(dropzone_data, sku)
|
getEvent(dropzone_data, sku)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听模型加载完成事件
|
||||||
|
kernel.on('model:loaded', (data) => {
|
||||||
|
// 隐藏进度条
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 存储当前选中的材质名和网格
|
// 存储当前选中的材质名和网格
|
||||||
@ -1045,14 +927,12 @@
|
|||||||
let currentPickedMesh = null;
|
let currentPickedMesh = null;
|
||||||
|
|
||||||
kernel.on('model:click', (data) => {
|
kernel.on('model:click', (data) => {
|
||||||
console.log('模型点击事件', data);
|
|
||||||
console.log('模型控制类型:', data.modelControlType);
|
|
||||||
|
|
||||||
// 获取模型关联的 SKU
|
// 获取模型关联的 SKU
|
||||||
const modelName = data.modelName;
|
const modelName = data.modelName;
|
||||||
const sku = window.getSkuByModelId(modelName);
|
const sku = window.getSkuByModelId(modelName);
|
||||||
console.log('点击的模型ID:', modelName);
|
|
||||||
console.log('关联的SKU:', sku || '未找到关联的SKU');
|
|
||||||
|
|
||||||
switch (data.modelControlType) {
|
switch (data.modelControlType) {
|
||||||
case "color":
|
case "color":
|
||||||
@ -1114,9 +994,10 @@
|
|||||||
|
|
||||||
// 暴露 kernel 到全局,方便调试
|
// 暴露 kernel 到全局,方便调试
|
||||||
|
|
||||||
|
kernel.on('model:loaded', (event) => {
|
||||||
|
});
|
||||||
|
|
||||||
kernel.on('hotspot:click', (event) => {
|
kernel.on('hotspot:click', (event) => {
|
||||||
console.log('热点被点击:', event);
|
|
||||||
|
|
||||||
const { id, name, payload } = event;
|
const { id, name, payload } = event;
|
||||||
|
|
||||||
|
|||||||
130
index.js
@ -5,6 +5,10 @@ import { setSkuMapping, getSkuByModelId, clearSkuMapping, clearAllSkuMappings }
|
|||||||
// 存储 kernel 实例
|
// 存储 kernel 实例
|
||||||
let kernelInstance = null;
|
let kernelInstance = null;
|
||||||
|
|
||||||
|
// 存储已加载的墙面配置(key为墙面名称,value为墙面配置)
|
||||||
|
// 用于拖拽时能找到对应的墙面配置
|
||||||
|
let wall_divisions_cache = new Map();
|
||||||
|
|
||||||
// 导出 SKU 映射相关函数,方便外部使用
|
// 导出 SKU 映射相关函数,方便外部使用
|
||||||
export { getSkuByModelId, clearSkuMapping, clearAllSkuMappings };
|
export { getSkuByModelId, clearSkuMapping, clearAllSkuMappings };
|
||||||
|
|
||||||
@ -18,7 +22,6 @@ export const initApp = (kernel) => {
|
|||||||
throw new Error('kernel 实例是必需的');
|
throw new Error('kernel 实例是必需的');
|
||||||
}
|
}
|
||||||
kernelInstance = kernel;
|
kernelInstance = kernel;
|
||||||
console.log('应用逻辑已初始化,kernel 实例已注入');
|
|
||||||
return kernelInstance;
|
return kernelInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -44,6 +47,10 @@ export const init = async (customConfig = {}) => {
|
|||||||
container: document.querySelector('#renderDom'),
|
container: document.querySelector('#renderDom'),
|
||||||
modelUrlList: [],
|
modelUrlList: [],
|
||||||
env: { envPath: 'https://cdn.files.zguiy.com/zt/environment.env', intensity: 1.2, rotationY: 0.3, background: false },
|
env: { envPath: 'https://cdn.files.zguiy.com/zt/environment.env', intensity: 1.2, rotationY: 0.3, background: false },
|
||||||
|
camera: {
|
||||||
|
position: { x: 5, y: 2, z: 7 }, // 相机位置:x-左右,y-上下,z-前后
|
||||||
|
target: { x: 0, y: 1, z: 0 } // 相机目标点:相机看向的位置
|
||||||
|
},
|
||||||
gizmo: {
|
gizmo: {
|
||||||
position: false,
|
position: false,
|
||||||
rotation: false,
|
rotation: false,
|
||||||
@ -58,11 +65,10 @@ export const init = async (customConfig = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 合并用户自定义配置
|
|
||||||
const config = { ...defaultConfig, ...customConfig };
|
const config = { ...defaultConfig, ...customConfig };
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
}
|
}
|
||||||
|
//
|
||||||
//初始化加载模型
|
//初始化加载模型
|
||||||
export const getAutoLoadModelList = async () => {
|
export const getAutoLoadModelList = async () => {
|
||||||
const kernel = getKernel();
|
const kernel = getKernel();
|
||||||
@ -98,6 +104,7 @@ export const getAutoLoadModelList = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//获取放置区域
|
//获取放置区域
|
||||||
export const getPlacementZone = async (sku) => {
|
export const getPlacementZone = async (sku) => {
|
||||||
//pergolaSku 是需要在加载棚子的时取其引用,传进来的sku则是配件的sku,根据配件的sku来判断放置区域
|
//pergolaSku 是需要在加载棚子的时取其引用,传进来的sku则是配件的sku,根据配件的sku来判断放置区域
|
||||||
@ -105,10 +112,12 @@ export const getPlacementZone = async (sku) => {
|
|||||||
let division_include = []
|
let division_include = []
|
||||||
// 同时包含10和13
|
// 同时包含10和13
|
||||||
const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku)
|
const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku)
|
||||||
// 只包含10 无13 无12
|
// 只包含10 无13 无12 无20
|
||||||
const only10 = /(?=.*10)(?!.*13)(?!.*12)/.test(pergolaSku)
|
const only10 = /(?=.*10)(?!.*13)(?!.*12)(?!.*20)/.test(pergolaSku)
|
||||||
// 同时包含10和12
|
// 同时包含10和12
|
||||||
const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku)
|
const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku)
|
||||||
|
// 同时包含10和20
|
||||||
|
const only10_20 = /(?=.*10)(?=.*20)/.test(pergolaSku)
|
||||||
|
|
||||||
// 1. 只要字符串里包含 10,就返回 true
|
// 1. 只要字符串里包含 10,就返回 true
|
||||||
const has10 = /10/.test(sku);
|
const has10 = /10/.test(sku);
|
||||||
@ -139,6 +148,26 @@ export const getPlacementZone = async (sku) => {
|
|||||||
if (only10_12 && has10) {
|
if (only10_12 && has10) {
|
||||||
division_include.push('左', '右')
|
division_include.push('左', '右')
|
||||||
}
|
}
|
||||||
|
//棚子同时包10和20的并且含配件是10
|
||||||
|
if (only10_20 && has10) {
|
||||||
|
|
||||||
|
if (pergolaSku === "SPF111S1020PILLAR4PCS") {
|
||||||
|
division_include.push('左', '右')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
division_include.push('前', '后', '左', '右', "前1", "后1", "前2", "后2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//棚子同时包10和20的并且含配件是13
|
||||||
|
if (only10_20 && has13) {
|
||||||
|
|
||||||
|
if (pergolaSku === "SPF111S1020PILLAR4PCS") {
|
||||||
|
division_include.push('前', '后')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[放置区域] 本次配件的方向:', division_include);
|
||||||
|
|
||||||
const response = await fetch(apiConfig.getApiUrl(`/api/product-configs/by-sku/${sku}`));
|
const response = await fetch(apiConfig.getApiUrl(`/api/product-configs/by-sku/${sku}`));
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@ -147,23 +176,70 @@ export const getPlacementZone = async (sku) => {
|
|||||||
const { enable_placement_zone, wall_divisions } = result.data;
|
const { enable_placement_zone, wall_divisions } = result.data;
|
||||||
// const {position_x, position_y, position_z} = data;
|
// const {position_x, position_y, position_z} = data;
|
||||||
if (enable_placement_zone && wall_divisions != undefined) {
|
if (enable_placement_zone && wall_divisions != undefined) {
|
||||||
console.log(wall_divisions);
|
console.log('[放置区域] 当前配件的墙面配置:', wall_divisions);
|
||||||
|
|
||||||
|
|
||||||
const filteredDivisions = wall_divisions.filter(item => division_include.includes(item.name))
|
const filteredDivisions = wall_divisions.filter(item => division_include.includes(item.name))
|
||||||
console.log(filteredDivisions);
|
console.log('[放置区域] 当前显示的墙面:', filteredDivisions);
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
|
||||||
kernel.dropZone.clearZones();
|
// 不需要手动 clearZones,updateDivisions 会自动处理增量更新
|
||||||
const divisions = filteredDivisions.map(wall => ({
|
const divisions = filteredDivisions.map(wall => ({
|
||||||
name: wall.name,
|
name: wall.name,
|
||||||
divisions: wall.divisions
|
divisions: wall.divisions
|
||||||
}))
|
}))
|
||||||
|
|
||||||
kernel.dropZone.updateDivisions(divisions);
|
const zones = kernel.dropZone.updateDivisions(divisions);
|
||||||
|
|
||||||
|
// 隐藏所有,然后只显示当前需要的墙面
|
||||||
|
kernel.dropZone.hide();
|
||||||
|
// 从生成的 zones 中提取完整的墙面名称
|
||||||
|
const wallNamesToShow = new Set(zones.map(zone => zone.wallName));
|
||||||
|
wallNamesToShow.forEach(wallName => {
|
||||||
|
kernel.dropZone.showWall(wallName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空墙面配置缓存
|
||||||
|
* 当需要重新开始配件布局时调用此方法
|
||||||
|
*/
|
||||||
|
export const clearDivisionCache = () => {
|
||||||
|
wall_divisions_cache.clear();
|
||||||
|
console.log('[放置区域] 墙面配置缓存已清空');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据墙面名称获取缓存的墙面配置
|
||||||
|
* @param {string} wallName 墙面名称
|
||||||
|
* @returns {Object|null} 墙面配置对象,如果没有则返回 null
|
||||||
|
*/
|
||||||
|
export const getCachedWallConfig = (wallName) => {
|
||||||
|
return wall_divisions_cache.get(wallName) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示指定墙面的放置区域(从缓存恢复)
|
||||||
|
* @param {string} wallName 墙面名称
|
||||||
|
*/
|
||||||
|
export const showWallFromCache = (wallName) => {
|
||||||
|
const kernel = getKernel();
|
||||||
|
const wallConfig = wall_divisions_cache.get(wallName);
|
||||||
|
|
||||||
|
if (wallConfig) {
|
||||||
|
console.log(`[放置区域] 从缓存恢复墙面 ${wallName}:`, wallConfig);
|
||||||
|
|
||||||
|
// 不需要手动 clearZones,updateDivisions 会自动处理增量更新
|
||||||
|
// 重新生成该墙面的放置区域
|
||||||
|
kernel.dropZone.updateDivisions([wallConfig]);
|
||||||
|
|
||||||
// 显示放置区域
|
// 显示放置区域
|
||||||
kernel.dropZone.show();
|
kernel.dropZone.show();
|
||||||
|
} else {
|
||||||
|
console.warn(`[放置区域] 墙面 ${wallName} 没有缓存配置`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
//执行事件
|
//执行事件
|
||||||
export const getEvent = async (dropzone_data, sku) => {
|
export const getEvent = async (dropzone_data, sku) => {
|
||||||
@ -174,8 +250,6 @@ export const getEvent = async (dropzone_data, sku) => {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.code === 200 && result.data) {
|
if (result.code === 200 && result.data) {
|
||||||
console.log('SKU配置数据:', result.data);
|
|
||||||
console.log('关联事件:', result.data.events);
|
|
||||||
|
|
||||||
// 使用 for...of 循环以支持 await
|
// 使用 for...of 循环以支持 await
|
||||||
await executeEvent(dropzone_data, result, sku)
|
await executeEvent(dropzone_data, result, sku)
|
||||||
@ -201,6 +275,7 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
// 第一次循环:处理 change_model
|
// 第一次循环:处理 change_model
|
||||||
for (const event of result.data.events) {
|
for (const event of result.data.events) {
|
||||||
if (event.event_type === 'change_model') {
|
if (event.event_type === 'change_model') {
|
||||||
|
|
||||||
const { name, file_url, model_control_type, category } = event.target_data;
|
const { name, file_url, model_control_type, category } = event.target_data;
|
||||||
|
|
||||||
// 生成唯一的模型ID
|
// 生成唯一的模型ID
|
||||||
@ -208,8 +283,7 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
modelName = name;
|
modelName = name;
|
||||||
kernel.dropZone.recordModelPlacement(wallName, index, name + '_' + modelId);
|
kernel.dropZone.recordModelPlacement(wallName, index, name + '_' + modelId);
|
||||||
|
|
||||||
// 记录模型ID到SKU的映射
|
|
||||||
setSkuMapping(modelId, sku);
|
|
||||||
|
|
||||||
await kernel.model.add({
|
await kernel.model.add({
|
||||||
modelName: name,
|
modelName: name,
|
||||||
@ -220,6 +294,10 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
enable: true,
|
enable: true,
|
||||||
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
|
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
|
snapToZone: true, // 拖拽吸附到最近的分割区域
|
||||||
|
returnWhenOutOfBounds: true, // 拖拽到区域外时返回原位置
|
||||||
|
handleOccupiedZone: true, // 处理已占用区域(false=允许重叠)
|
||||||
|
occupiedZoneAction: 'replace' // 当开关3=true时的行为:'return'=返回原位置,'replace'=替换
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
position: position,
|
position: position,
|
||||||
@ -227,7 +305,6 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`百叶模型已放置为 ${name + '_' + modelId}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +314,8 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
const materialName = event.material_name;
|
const materialName = event.material_name;
|
||||||
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
|
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
kernel.material.apply({
|
kernel.material.apply({
|
||||||
target: materialName,
|
target: materialName,
|
||||||
modelId: modelName + '_' + modelId, // 传入 modelId,只替换该模型的材质
|
modelId: modelName + '_' + modelId, // 传入 modelId,只替换该模型的材质
|
||||||
@ -247,7 +326,6 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
roughness: +roughness
|
roughness: +roughness
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +340,6 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('当前棚子的 SKU:', pergolaSku);
|
|
||||||
return pergolaSku;
|
return pergolaSku;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,26 +358,24 @@ export const executeEvent2 = async (result, sku) => {
|
|||||||
if (firstModelEvent && firstModelEvent.target_data) {
|
if (firstModelEvent && firstModelEvent.target_data) {
|
||||||
const { name, category } = firstModelEvent.target_data;
|
const { name, category } = firstModelEvent.target_data;
|
||||||
modelAlreadyExists = kernel.model.exists(name + '_' + category);
|
modelAlreadyExists = kernel.model.exists(name + '_' + category);
|
||||||
console.log(`检查模型 ${name + '_' + category} 是否存在:`, modelAlreadyExists);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kernel.dropZone.hide();
|
kernel.dropZone.hide();
|
||||||
// 只有在需要更换模型且模型不存在时才清除
|
// 只有在需要更换模型且模型不存在时才清除
|
||||||
if (hasModelChange && !modelAlreadyExists) {
|
if (hasModelChange && !modelAlreadyExists) {
|
||||||
console.log('模型不存在,执行清除操作');
|
|
||||||
|
|
||||||
kernel.model.removeAll();
|
kernel.model.removeAll();
|
||||||
// 清除所有 SKU 映射
|
// 清除所有 SKU 映射
|
||||||
clearAllSkuMappings();
|
clearAllSkuMappings();
|
||||||
|
// 只清除放置区域的网格和数据,不删除模型(模型已经在 removeAll 中删除了)
|
||||||
|
kernel.dropZone.clearZones();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先处理所有 change_model 事件
|
// 先处理所有 change_model 事件
|
||||||
for (const event of result.data.events) {
|
for (const event of result.data.events) {
|
||||||
console.log(event);
|
|
||||||
|
|
||||||
if (event.event_type === 'change_model') {
|
if (event.event_type === 'change_model') {
|
||||||
const { target_data } = event;
|
const { target_data } = event;
|
||||||
console.log(event.target_data);
|
|
||||||
if (!target_data) {
|
if (!target_data) {
|
||||||
console.error('change_model事件缺少target_data')
|
console.error('change_model事件缺少target_data')
|
||||||
return;
|
return;
|
||||||
@ -309,7 +384,6 @@ export const executeEvent2 = async (result, sku) => {
|
|||||||
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
|
const { id, name, file_url, model_control_type, category, placement_zone } = target_data;
|
||||||
// 如果模型已存在,跳过加载
|
// 如果模型已存在,跳过加载
|
||||||
if (modelAlreadyExists) {
|
if (modelAlreadyExists) {
|
||||||
console.log(`模型 ${name + '_' + category} 已存在,跳过加载`);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +410,6 @@ export const executeEvent2 = async (result, sku) => {
|
|||||||
modelControlType: model_control_type,
|
modelControlType: model_control_type,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`模型已放置为 ${name + '_' + category}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,16 +418,16 @@ export const executeEvent2 = async (result, sku) => {
|
|||||||
if (event.event_type === 'change_color') {
|
if (event.event_type === 'change_color') {
|
||||||
const materialName = event.material_name;
|
const materialName = event.material_name;
|
||||||
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
|
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
|
||||||
console.log('替换模型颜色:', event.target_data);
|
|
||||||
|
|
||||||
kernel.material.apply({
|
kernel.material.apply({
|
||||||
target: materialName,
|
target: materialName,
|
||||||
albedoColor: color,
|
albedoColor: color,
|
||||||
albedoTexture: color_map_url,
|
albedoTexture: color_map_url,
|
||||||
normalMap: normal_map_url,
|
normalMap: normal_map_url,
|
||||||
|
metallic: +metallic,
|
||||||
|
roughness: +roughness,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,7 +460,6 @@ export const getHotspot = async () => {
|
|||||||
|
|
||||||
// 渲染热点
|
// 渲染热点
|
||||||
kernel.hotspot.render(hotspots);
|
kernel.hotspot.render(hotspots);
|
||||||
console.log('热点渲染成功:', hotspots);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('没有可用的热点数据');
|
console.log('没有可用的热点数据');
|
||||||
}
|
}
|
||||||
@ -397,11 +469,11 @@ export const getHotspot = async () => {
|
|||||||
}
|
}
|
||||||
//点击右侧按钮自动判断
|
//点击右侧按钮自动判断
|
||||||
export const getProductConfig = async (sku) => {
|
export const getProductConfig = async (sku) => {
|
||||||
|
console.log(sku);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiConfig.getApiUrl(`/api/product-configs/by-sku/${sku}`)}`);
|
const response = await fetch(`${apiConfig.getApiUrl(`/api/product-configs/by-sku/${sku}`)}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.code === 200) {
|
if (result.code === 200) {
|
||||||
console.log(result.data);
|
|
||||||
const { enable_placement_zone } = result.data;
|
const { enable_placement_zone } = result.data;
|
||||||
//如果触发的是配件,需要显示放置区域
|
//如果触发的是配件,需要显示放置区域
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,18 @@ export class AppCamera extends Monobehiver {
|
|||||||
const canvas = AppConfig.container;
|
const canvas = AppConfig.container;
|
||||||
if (!scene || !canvas) return;
|
if (!scene || !canvas) return;
|
||||||
|
|
||||||
// 创建弧形旋转相机:水平角70度,垂直角85度(接近上帝视角),距离5,目标点(0,2,0)
|
// 从配置中获取相机参数
|
||||||
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
|
const { position, target } = AppConfig.camera;
|
||||||
|
|
||||||
|
// 创建弧形旋转相机:水平角70度,垂直角85度(接近上帝视角),距离5,目标点从配置读取
|
||||||
|
this.object = new ArcRotateCamera(
|
||||||
|
'Camera',
|
||||||
|
Tools.ToRadians(70),
|
||||||
|
Tools.ToRadians(85),
|
||||||
|
5,
|
||||||
|
new Vector3(target.x, target.y, target.z),
|
||||||
|
scene
|
||||||
|
);
|
||||||
this.object.attachControl(canvas, true);
|
this.object.attachControl(canvas, true);
|
||||||
this.object.minZ = 0.01; // 近裁剪面
|
this.object.minZ = 0.01; // 近裁剪面
|
||||||
this.object.wheelPrecision = 200; // 滚轮缩放精度
|
this.object.wheelPrecision = 200; // 滚轮缩放精度
|
||||||
@ -31,8 +41,9 @@ export class AppCamera extends Monobehiver {
|
|||||||
// 限制垂直角范围,实现上帝视角
|
// 限制垂直角范围,实现上帝视角
|
||||||
this.object.upperBetaLimit = Tools.ToRadians(90); // 最大垂直角(接近90度,避免万向锁)
|
this.object.upperBetaLimit = Tools.ToRadians(90); // 最大垂直角(接近90度,避免万向锁)
|
||||||
|
|
||||||
this.object.position = new Vector3(0,0, 10);
|
// 设置相机位置(从配置读取)
|
||||||
this.setTarget(0, 0.5, 0);
|
this.object.position = new Vector3(position.x, position.y, position.z);
|
||||||
|
this.setTarget(target.x, target.y, target.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置相机目标点 */
|
/** 设置相机目标点 */
|
||||||
|
|||||||
@ -14,6 +14,10 @@ export const AppConfig = {
|
|||||||
rotationY: 0,
|
rotationY: 0,
|
||||||
background: false,
|
background: false,
|
||||||
},
|
},
|
||||||
|
camera: {
|
||||||
|
position: { x: 0, y: 2, z: 5 },
|
||||||
|
target: { x: 0, y: 1, z: 0 },
|
||||||
|
},
|
||||||
gizmo: {
|
gizmo: {
|
||||||
position: true,
|
position: true,
|
||||||
rotation: false,
|
rotation: false,
|
||||||
|
|||||||
@ -35,6 +35,9 @@ export class AppDropZone {
|
|||||||
// 存储原始墙面配置(用于 updateDivisions 时恢复完整墙面列表)
|
// 存储原始墙面配置(用于 updateDivisions 时恢复完整墙面列表)
|
||||||
private originalWalls: WallConfig[] = [];
|
private originalWalls: WallConfig[] = [];
|
||||||
|
|
||||||
|
// 备份数据,用于点击空白处时回退
|
||||||
|
private backupConfig: DropZoneConfig | null = null;
|
||||||
|
|
||||||
constructor(scene: Scene) {
|
constructor(scene: Scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.placementWall = new AppPlacementWall(scene);
|
this.placementWall = new AppPlacementWall(scene);
|
||||||
@ -93,43 +96,29 @@ export class AppDropZone {
|
|||||||
return this.placementWall.generatePlacementAreas(configWithDivisions);
|
return this.placementWall.generatePlacementAreas(configWithDivisions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新墙面分割数并重新生成放置区域
|
|
||||||
* @param divisions 分割数数组,每个元素包含 name(墙面名称或方向标识)和 divisions(分割数)
|
|
||||||
* @example
|
|
||||||
* // 方式1:精确匹配墙面名称(推荐)
|
|
||||||
* updateDivisions([
|
|
||||||
* { name: "前1", divisions: 1 },
|
|
||||||
* { name: "前2", divisions: 2 },
|
|
||||||
* { name: "左", divisions: 3 },
|
|
||||||
* { name: "右", divisions: 4 }
|
|
||||||
* ])
|
|
||||||
*
|
|
||||||
* // 方式2:模糊匹配方向(兼容旧版)
|
|
||||||
* updateDivisions([
|
|
||||||
* { name: "前", divisions: 4 },
|
|
||||||
* { name: "后", divisions: 1 }
|
|
||||||
* ])
|
|
||||||
*/
|
|
||||||
updateDivisions(divisions: Array<{ name: string; divisions: number }>): PlacementZoneInfo[] {
|
updateDivisions(divisions: Array<{ name: string; divisions: number }>): PlacementZoneInfo[] {
|
||||||
if (!this.dropZoneConfig) {
|
if (!this.dropZoneConfig) {
|
||||||
console.error('未设置放置区域配置数据,请先调用 setData');
|
console.error('未设置放置区域配置数据,请先调用 setData');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 每次 updateDivisions 都备份当前配置(深拷贝,保留 Vector3 对象)
|
||||||
|
this.backupConfig = {
|
||||||
|
...this.dropZoneConfig,
|
||||||
|
walls: this.dropZoneConfig.walls.map(wall => ({
|
||||||
|
...wall,
|
||||||
|
startPoint: wall.startPoint.clone(),
|
||||||
|
endPoint: wall.endPoint.clone()
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
// 将数组转换为对象映射
|
// 将数组转换为对象映射
|
||||||
const divisionsMap: Record<string, number> = {};
|
const divisionsMap: Record<string, number> = {};
|
||||||
divisions.forEach(item => {
|
divisions.forEach(item => {
|
||||||
divisionsMap[item.name] = item.divisions;
|
divisionsMap[item.name] = item.divisions;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定义方向关键字映射(支持中英文)
|
|
||||||
const directionKeywords: Record<string, string[]> = {
|
|
||||||
'前': ['前', 'front', 'qian'],
|
|
||||||
'后': ['后', 'back', 'hou'],
|
|
||||||
'左': ['左', 'left', 'zuo'],
|
|
||||||
'右': ['右', 'right', 'you']
|
|
||||||
};
|
|
||||||
|
|
||||||
// 匹配墙面名称(精确匹配)
|
// 匹配墙面名称(精确匹配)
|
||||||
const matchWallName = (wallName: string): number | null => {
|
const matchWallName = (wallName: string): number | null => {
|
||||||
@ -139,16 +128,14 @@ export class AppDropZone {
|
|||||||
|
|
||||||
// 精确匹配提取出的简短名称
|
// 精确匹配提取出的简短名称
|
||||||
if (divisionsMap[wallShortName] !== undefined) {
|
if (divisionsMap[wallShortName] !== undefined) {
|
||||||
console.log(`墙面 "${wallName}" 通过简短名称 "${wallShortName}" 精确匹配到分割数: ${divisionsMap[wallShortName]}`);
|
|
||||||
return divisionsMap[wallShortName];
|
return divisionsMap[wallShortName];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`墙面 "${wallName}" 未匹配到任何分割数配置`);
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新配置中的墙面分割数,从原始配置中恢复墙面列表
|
// 从原始配置中筛选出本次要更新的墙面
|
||||||
this.dropZoneConfig.walls = this.originalWalls
|
const newWalls = this.originalWalls
|
||||||
.map(wall => {
|
.map(wall => {
|
||||||
const newDivisions = matchWallName(wall.name);
|
const newDivisions = matchWallName(wall.name);
|
||||||
|
|
||||||
@ -157,20 +144,35 @@ export class AppDropZone {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`墙面 "${wall.name}" 匹配到分割数: ${newDivisions}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...wall,
|
...wall,
|
||||||
divisions: newDivisions
|
divisions: newDivisions
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面
|
.filter(wall => wall !== null) as WallConfig[];
|
||||||
|
|
||||||
// 清除旧的放置区域网格(不清除模型)
|
// 合并到现有配置中(保留其他墙面,更新/添加本次传入的墙面)
|
||||||
this.clearZones();
|
// 先过滤掉 divisions 为 0 或未设置的墙面(避免初始状态污染)
|
||||||
|
const existingWallsMap = new Map(
|
||||||
|
this.dropZoneConfig.walls
|
||||||
|
.filter(w => w.divisions && w.divisions > 0) // 只保留有效的墙面配置
|
||||||
|
.map(w => [w.name, w])
|
||||||
|
);
|
||||||
|
newWalls.forEach(wall => {
|
||||||
|
existingWallsMap.set(wall.name, wall);
|
||||||
|
});
|
||||||
|
this.dropZoneConfig.walls = Array.from(existingWallsMap.values());
|
||||||
|
|
||||||
// 重新生成放置区域
|
// 更新 wallDivisionsMap(重要:用于后续的自动排列和拖拽检查)
|
||||||
const zones = this.generateDropZones();
|
this.dropZoneConfig.walls.forEach(wall => {
|
||||||
|
this.wallDivisionsMap.set(wall.name, wall.divisions);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只生成本次传入的墙面(不生成所有墙面)
|
||||||
|
const zones = this.placementWall.generatePlacementAreas({
|
||||||
|
...this.dropZoneConfig,
|
||||||
|
walls: newWalls // 只传入本次要更新的墙面
|
||||||
|
});
|
||||||
|
|
||||||
// 显示放置区域
|
// 显示放置区域
|
||||||
this.show();
|
this.show();
|
||||||
@ -206,10 +208,6 @@ export class AppDropZone {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
keysToDelete.forEach(key => this.zoneModelMap.delete(key));
|
keysToDelete.forEach(key => this.zoneModelMap.delete(key));
|
||||||
|
|
||||||
if (modelsToUnload.length > 0) {
|
|
||||||
console.log(`已卸载墙面 ${wallName} 的 ${modelsToUnload.length} 个模型`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -223,7 +221,6 @@ export class AppDropZone {
|
|||||||
// 检查分割数是否改变
|
// 检查分割数是否改变
|
||||||
if (modelDivisions !== undefined && currentDivisions !== undefined && modelDivisions !== currentDivisions) {
|
if (modelDivisions !== undefined && currentDivisions !== undefined && modelDivisions !== currentDivisions) {
|
||||||
// 分割数改变了,清空该墙面的所有旧模型
|
// 分割数改变了,清空该墙面的所有旧模型
|
||||||
console.log(`墙面 ${wallName} 分割数从 ${modelDivisions} 改为 ${currentDivisions},清空旧模型`);
|
|
||||||
this.unloadWallModels(wallName);
|
this.unloadWallModels(wallName);
|
||||||
// 更新该墙面模型对应的分割数
|
// 更新该墙面模型对应的分割数
|
||||||
this.wallModelDivisionsMap.set(wallName, currentDivisions);
|
this.wallModelDivisionsMap.set(wallName, currentDivisions);
|
||||||
@ -231,7 +228,6 @@ export class AppDropZone {
|
|||||||
// 分割数没变,检查该区域是否已有模型(替换逻辑)
|
// 分割数没变,检查该区域是否已有模型(替换逻辑)
|
||||||
const existingModelId = this.zoneModelMap.get(zoneKey);
|
const existingModelId = this.zoneModelMap.get(zoneKey);
|
||||||
if (existingModelId && this.appModel) {
|
if (existingModelId && this.appModel) {
|
||||||
console.log(`区域 ${zoneKey} 已有模型 ${existingModelId},将替换为 ${modelId}`);
|
|
||||||
this.appModel.removeByName(existingModelId);
|
this.appModel.removeByName(existingModelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,7 +239,9 @@ export class AppDropZone {
|
|||||||
|
|
||||||
// 记录新模型
|
// 记录新模型
|
||||||
this.zoneModelMap.set(zoneKey, modelId);
|
this.zoneModelMap.set(zoneKey, modelId);
|
||||||
console.log(`已记录模型 ${modelId} 到区域 ${zoneKey}`);
|
|
||||||
|
// 成功放置模型,确认当前配置(清除备份)
|
||||||
|
this.confirmConfig();
|
||||||
|
|
||||||
// 检查该墙面是否已满,如果满了则自动排列
|
// 检查该墙面是否已满,如果满了则自动排列
|
||||||
this.checkAndAutoArrange(wallName);
|
this.checkAndAutoArrange(wallName);
|
||||||
@ -254,7 +252,7 @@ export class AppDropZone {
|
|||||||
* @param modelId 被删除的模型ID
|
* @param modelId 被删除的模型ID
|
||||||
*/
|
*/
|
||||||
notifyModelRemoved(modelId: string): void {
|
notifyModelRemoved(modelId: string): void {
|
||||||
console.log(`[模型删除通知] 模型 ${modelId} 被删除`);
|
|
||||||
|
|
||||||
// 找到该模型所在的墙面和索引
|
// 找到该模型所在的墙面和索引
|
||||||
let removedWallName: string | null = null;
|
let removedWallName: string | null = null;
|
||||||
@ -274,7 +272,6 @@ export class AppDropZone {
|
|||||||
if (removedZoneKey) {
|
if (removedZoneKey) {
|
||||||
// 从映射中删除
|
// 从映射中删除
|
||||||
this.zoneModelMap.delete(removedZoneKey);
|
this.zoneModelMap.delete(removedZoneKey);
|
||||||
console.log(`[模型删除通知] 已从映射中删除: ${removedZoneKey}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedWallName) {
|
if (removedWallName) {
|
||||||
@ -301,15 +298,15 @@ export class AppDropZone {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[拖拽检查] 墙面 ${wallName} 当前模型数: ${placedCount}/${currentDivisions}`);
|
|
||||||
|
|
||||||
// 如果墙面不满,重新启用所有模型的拖拽
|
// 如果墙面不满,重新启用所有模型的拖拽
|
||||||
if (placedCount < currentDivisions) {
|
if (placedCount < currentDivisions) {
|
||||||
console.log(`[拖拽检查] 墙面 ${wallName} 未满,重新启用拖拽`);
|
|
||||||
placedModelIds.forEach(modelId => {
|
placedModelIds.forEach(modelId => {
|
||||||
if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === 'function') {
|
if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === 'function') {
|
||||||
this.mainApp.appModelDrag.setDragEnabled(modelId, true);
|
this.mainApp.appModelDrag.setDragEnabled(modelId, true);
|
||||||
console.log(`[拖拽检查] ✓ 已启用模型 ${modelId} 的拖拽功能`);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -341,10 +338,8 @@ export class AppDropZone {
|
|||||||
*/
|
*/
|
||||||
private checkAndAutoArrange(wallName: string): void {
|
private checkAndAutoArrange(wallName: string): void {
|
||||||
const currentDivisions = this.wallDivisionsMap.get(wallName);
|
const currentDivisions = this.wallDivisionsMap.get(wallName);
|
||||||
console.log(`[自动排列检查] 墙面: ${wallName}, 分割数: ${currentDivisions}`);
|
|
||||||
|
|
||||||
if (!currentDivisions) {
|
if (!currentDivisions) {
|
||||||
console.log(`[自动排列检查] 墙面 ${wallName} 没有分割数配置,跳过`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,15 +353,11 @@ export class AppDropZone {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[自动排列检查] 墙面 ${wallName} 已放置模型数: ${placedCount}/${currentDivisions}`);
|
|
||||||
console.log(`[自动排列检查] 已放置的模型:`, placedModels);
|
|
||||||
|
|
||||||
// 如果该墙面已满(放置数量等于分割数),执行自动排列
|
// 如果该墙面已满(放置数量等于分割数),执行自动排列
|
||||||
if (placedCount === currentDivisions) {
|
if (placedCount === currentDivisions) {
|
||||||
console.log(`[自动排列] 墙面 ${wallName} 已满(${placedCount}/${currentDivisions}),开始执行自动排列`);
|
|
||||||
this.autoArrangeWall(wallName);
|
this.autoArrangeWall(wallName);
|
||||||
} else {
|
|
||||||
console.log(`[自动排列检查] 墙面 ${wallName} 未满,不执行自动排列`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,14 +366,12 @@ export class AppDropZone {
|
|||||||
* @param wallName 墙面名称
|
* @param wallName 墙面名称
|
||||||
*/
|
*/
|
||||||
private autoArrangeWall(wallName: string): void {
|
private autoArrangeWall(wallName: string): void {
|
||||||
console.log(`[自动排列] 开始排列墙面: ${wallName}`);
|
|
||||||
|
|
||||||
// 获取该墙面的所有放置区域
|
// 获取该墙面的所有放置区域
|
||||||
const wallZones = this.getZonesByWall(wallName);
|
const wallZones = this.getZonesByWall(wallName);
|
||||||
console.log(`[自动排列] 墙面 ${wallName} 的放置区域数量: ${wallZones.length}`);
|
|
||||||
|
|
||||||
if (!wallZones.length) {
|
if (!wallZones.length) {
|
||||||
console.log(`[自动排列] 墙面 ${wallName} 没有放置区域,退出`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,19 +385,16 @@ export class AppDropZone {
|
|||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
currentIndex: currentIndex
|
currentIndex: currentIndex
|
||||||
});
|
});
|
||||||
console.log(`[自动排列] 找到模型: ${modelId}, 当前索引: ${currentIndex}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[自动排列] 收集到 ${placedModels.length} 个模型`);
|
|
||||||
|
|
||||||
// 按当前索引排序
|
// 按当前索引排序
|
||||||
placedModels.sort((a, b) => a.currentIndex - b.currentIndex);
|
placedModels.sort((a, b) => a.currentIndex - b.currentIndex);
|
||||||
console.log(`[自动排列] 排序后的模型顺序:`, placedModels.map(m => `${m.modelId}(索引${m.currentIndex})`));
|
|
||||||
|
|
||||||
// 重新排列:将模型按顺序放置到 0, 1, 2... 的位置
|
// 重新排列:将模型按顺序放置到 0, 1, 2... 的位置
|
||||||
placedModels.forEach((model, newIndex) => {
|
placedModels.forEach((model, newIndex) => {
|
||||||
console.log(`[自动排列] 处理模型 ${model.modelId}: 当前索引=${model.currentIndex}, 目标索引=${newIndex}`);
|
|
||||||
|
|
||||||
// 获取目标放置区域
|
// 获取目标放置区域
|
||||||
const targetZone = wallZones[newIndex];
|
const targetZone = wallZones[newIndex];
|
||||||
@ -419,19 +405,13 @@ export class AppDropZone {
|
|||||||
|
|
||||||
if (this.appModel) {
|
if (this.appModel) {
|
||||||
// 计算新位置(从放置区域的中心点加上法线偏移)
|
// 计算新位置(从放置区域的中心点加上法线偏移)
|
||||||
const offsetDistance = 0.05;
|
const offsetDistance = 0; // 增加偏移距离,让模型更往外
|
||||||
const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
|
const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
|
||||||
|
|
||||||
// 计算旋转角度(根据法线方向)
|
// 计算旋转角度(根据法线方向)
|
||||||
const targetDirection = targetZone.normal.scale(-1);
|
const targetDirection = targetZone.normal.scale(-1);
|
||||||
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||||
|
|
||||||
console.log(`[自动排列] 目标区域 ${newIndex} 的位置:`, {
|
|
||||||
center: targetZone.center,
|
|
||||||
normal: targetZone.normal,
|
|
||||||
targetPosition: targetPosition,
|
|
||||||
rotation: angle * 180 / Math.PI
|
|
||||||
});
|
|
||||||
|
|
||||||
// 移动模型到新位置
|
// 移动模型到新位置
|
||||||
const meshes = this.appModel.getCachedMeshes(model.modelId);
|
const meshes = this.appModel.getCachedMeshes(model.modelId);
|
||||||
@ -444,7 +424,6 @@ export class AppDropZone {
|
|||||||
// 更新旋转
|
// 更新旋转
|
||||||
rootMesh.rotation.y = angle;
|
rootMesh.rotation.y = angle;
|
||||||
|
|
||||||
console.log(`[自动排列] ✓ 模型 ${model.modelId} 已移动到索引 ${newIndex} 的位置`);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[自动排列] ✗ 找不到模型 ${model.modelId} 的网格`);
|
console.warn(`[自动排列] ✗ 找不到模型 ${model.modelId} 的网格`);
|
||||||
}
|
}
|
||||||
@ -455,24 +434,20 @@ export class AppDropZone {
|
|||||||
const newKey = `${wallName}[${newIndex}]`;
|
const newKey = `${wallName}[${newIndex}]`;
|
||||||
this.zoneModelMap.delete(oldKey);
|
this.zoneModelMap.delete(oldKey);
|
||||||
this.zoneModelMap.set(newKey, model.modelId);
|
this.zoneModelMap.set(newKey, model.modelId);
|
||||||
console.log(`[自动排列] 更新映射: ${oldKey} -> ${newKey}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 禁用该墙面所有模型的拖拽功能
|
// 禁用该墙面所有模型的拖拽功能
|
||||||
console.log(`[自动排列] 开始禁用拖拽功能`);
|
|
||||||
placedModels.forEach(model => {
|
placedModels.forEach(model => {
|
||||||
// 安全检查:确保 mainApp 和 appModelDrag 都存在
|
// 安全检查:确保 mainApp 和 appModelDrag 都存在
|
||||||
if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === 'function') {
|
if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === 'function') {
|
||||||
this.mainApp.appModelDrag.setDragEnabled(model.modelId, false);
|
this.mainApp.appModelDrag.setDragEnabled(model.modelId, false);
|
||||||
console.log(`[自动排列] ✓ 已禁用模型 ${model.modelId} 的拖拽功能`);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[自动排列] ✗ 无法禁用模型 ${model.modelId} 的拖拽功能:appModelDrag 未初始化`);
|
console.warn(`[自动排列] ✗ 无法禁用模型 ${model.modelId} 的拖拽功能:appModelDrag 未初始化`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[自动排列] 墙面 ${wallName} 自动排列完成`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -507,20 +482,85 @@ export class AppDropZone {
|
|||||||
* 显示所有放置区域
|
* 显示所有放置区域
|
||||||
*/
|
*/
|
||||||
show(): void {
|
show(): void {
|
||||||
this.placementWall.show();
|
// this.placementWall.show();
|
||||||
|
// 禁用所有已放置模型的拾取
|
||||||
|
this.setModelsPickable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只显示指定墙面的放置区域
|
||||||
|
* @param wallName 墙面名称
|
||||||
|
*/
|
||||||
|
showWall(wallName: string): void {
|
||||||
|
this.placementWall.showWall(wallName);
|
||||||
|
// 禁用所有已放置模型的拾取
|
||||||
|
this.setModelsPickable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 隐藏所有放置区域
|
* 隐藏所有放置区域
|
||||||
|
* @param shouldRollback 是否回退到备份配置(点击空白处时为true)
|
||||||
*/
|
*/
|
||||||
hide(): void {
|
hide(shouldRollback: boolean = false): void {
|
||||||
|
// 如果需要回退且有备份数据,则恢复配置
|
||||||
|
if (shouldRollback && this.backupConfig) {
|
||||||
|
this.dropZoneConfig = this.backupConfig;
|
||||||
|
this.backupConfig = null;
|
||||||
|
|
||||||
|
// 同步 wallDivisionsMap
|
||||||
|
if (this.dropZoneConfig) {
|
||||||
|
this.dropZoneConfig.walls.forEach(wall => {
|
||||||
|
this.wallDivisionsMap.set(wall.name, wall.divisions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新生成放置区域,使 placementZones 与回退后的配置一致
|
||||||
|
this.placementWall.generatePlacementAreas({
|
||||||
|
...this.dropZoneConfig,
|
||||||
|
walls: this.dropZoneConfig.walls
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.placementWall.hide();
|
this.placementWall.hide();
|
||||||
|
// 恢复所有已放置模型的拾取
|
||||||
|
this.setModelsPickable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认当前配置(清除备份)
|
||||||
|
* 当成功放置配件后调用,表示接受当前的配置修改
|
||||||
|
*/
|
||||||
|
confirmConfig(): void {
|
||||||
|
this.backupConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置所有已放置模型的可拾取状态
|
||||||
|
* @param pickable 是否可拾取
|
||||||
|
*/
|
||||||
|
private setModelsPickable(pickable: boolean): void {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除所有放置区域(只清除网格,不清除模型)
|
* 清除所有放置区域(只清除网格,不清除模型)
|
||||||
*/
|
*/
|
||||||
clearZones(): void {
|
clearZones(): void {
|
||||||
|
// 清除映射(不删除模型,只清空记录)
|
||||||
|
this.zoneModelMap.clear();
|
||||||
|
this.wallDivisionsMap.clear();
|
||||||
|
this.wallModelDivisionsMap.clear();
|
||||||
|
|
||||||
|
// 清除放置区域的 mesh
|
||||||
this.placementWall.clearAll();
|
this.placementWall.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -97,9 +97,6 @@ export class AppGround extends Monobehiver {
|
|||||||
this.ground.position = this.config.position;
|
this.ground.position = this.config.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.receiveShadows) {
|
|
||||||
this.ground.receiveShadows = true;
|
|
||||||
}
|
|
||||||
this.ground.isPickable = false;
|
this.ground.isPickable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,6 @@ export class AppLight extends Monobehiver {
|
|||||||
Awake(): void {
|
Awake(): void {
|
||||||
// 主光源(模拟太阳)
|
// 主光源(模拟太阳)
|
||||||
const light = new DirectionalLight("sun", new Vector3(-1, -2, -1), this.mainApp.appScene.object);
|
const light = new DirectionalLight("sun", new Vector3(-1, -2, -1), this.mainApp.appScene.object);
|
||||||
light.intensity = 1.2;
|
light.intensity = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,749 +0,0 @@
|
|||||||
import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
|
|
||||||
import '@babylonjs/loaders/glTF';
|
|
||||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
|
||||||
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
||||||
import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
||||||
import { Scene } from '@babylonjs/core/scene';
|
|
||||||
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;
|
|
||||||
meshes?: AbstractMesh[];
|
|
||||||
skeletons?: unknown[];
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ModelConfig = {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ModelControlType = 'rotation' | 'color';
|
|
||||||
|
|
||||||
type ModelTransform = {
|
|
||||||
position?: { x: number; y: number; z: number };
|
|
||||||
rotation?: { x: number; y: number; z: number };
|
|
||||||
scale?: { x: number; y: number; z: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
type ModelMetadata = {
|
|
||||||
modelName: string;
|
|
||||||
modelId: string;
|
|
||||||
modelUrl: string;
|
|
||||||
modelControlType?: ModelControlType;
|
|
||||||
drag?: DragConfig;
|
|
||||||
transform?: ModelTransform;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模型管理类 - 负责加载、缓存和管理3D模型
|
|
||||||
*/
|
|
||||||
export class AppModel extends Monobehiver {
|
|
||||||
private modelDic: Dictionary<AbstractMesh[]>;
|
|
||||||
private modelMetadataDic: Dictionary<ModelMetadata>;
|
|
||||||
private loadedMeshes: AbstractMesh[];
|
|
||||||
private isLoading: boolean;
|
|
||||||
|
|
||||||
constructor(mainApp: any) {
|
|
||||||
super(mainApp);
|
|
||||||
this.modelDic = new Dictionary<AbstractMesh[]>();
|
|
||||||
this.modelMetadataDic = new Dictionary<ModelMetadata>();
|
|
||||||
this.loadedMeshes = [];
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
initManagers(): void {
|
|
||||||
// 预留接口
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 加载配置中的所有模型 */
|
|
||||||
async loadModel(): Promise<void> {
|
|
||||||
if (!AppConfig.modelUrlList?.length || this.isLoading) return;
|
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
await this.loadMultipleModels(AppConfig.modelUrlList);
|
|
||||||
EventBridge.modelLoaded({ urls: AppConfig.modelUrlList });
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量加载模型(内部方法)
|
|
||||||
* @param urls 模型URL数组
|
|
||||||
*/
|
|
||||||
private async loadMultipleModels(urls: string[]): Promise<void> {
|
|
||||||
const total = urls.length;
|
|
||||||
EventBridge.modelLoadProgress({ loaded: 0, total, urls, progress: 0, percentage: 0 });
|
|
||||||
|
|
||||||
for (let i = 0; i < urls.length; i++) {
|
|
||||||
const url = urls[i];
|
|
||||||
const result = await this.loadSingleModel(url, (event) => {
|
|
||||||
this.emitProgress(i, total, url, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emitProgress(i + 1, total, url, null, result.success);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
EventBridge.modelLoadError({ url, error: result.error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送加载进度事件
|
|
||||||
*/
|
|
||||||
private emitProgress(
|
|
||||||
loaded: number,
|
|
||||||
total: number,
|
|
||||||
url: string,
|
|
||||||
event: ISceneLoaderProgressEvent | null,
|
|
||||||
success?: boolean
|
|
||||||
): void {
|
|
||||||
const currentProgress = event?.lengthComputable && event.total > 0
|
|
||||||
? Math.min(1, event.loaded / event.total)
|
|
||||||
: 0;
|
|
||||||
const overallProgress = Math.min(1, (loaded + (event ? currentProgress : 0)) / total);
|
|
||||||
|
|
||||||
EventBridge.modelLoadProgress({
|
|
||||||
loaded: loaded + (event ? currentProgress : 0),
|
|
||||||
total,
|
|
||||||
url,
|
|
||||||
success,
|
|
||||||
progress: overallProgress,
|
|
||||||
percentage: Number((overallProgress * 100).toFixed(2)),
|
|
||||||
detail: event ? {
|
|
||||||
url,
|
|
||||||
lengthComputable: event.lengthComputable,
|
|
||||||
loadedBytes: event.loaded,
|
|
||||||
totalBytes: event.total
|
|
||||||
} : undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载单个模型文件
|
|
||||||
* @param modelUrl 模型URL
|
|
||||||
* @param onProgress 进度回调
|
|
||||||
*/
|
|
||||||
private async loadSingleModel(
|
|
||||||
modelUrl: string,
|
|
||||||
onProgress?: (event: ISceneLoaderProgressEvent) => void
|
|
||||||
): Promise<LoadResult> {
|
|
||||||
try {
|
|
||||||
const scene = this.mainApp.appScene.object;
|
|
||||||
if (!scene) return { success: false, error: '场景未初始化' };
|
|
||||||
|
|
||||||
const result = await ImportMeshAsync(modelUrl, scene, { onProgress });
|
|
||||||
if (!result?.meshes?.length) return { success: false, error: '未找到网格' };
|
|
||||||
|
|
||||||
this.loadedMeshes.push(...result.meshes);
|
|
||||||
return { success: true, meshes: result.meshes, skeletons: result.skeletons };
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`模型加载失败: ${modelUrl}`, e);
|
|
||||||
return { success: false, error: e?.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 克隆模型材质,避免多个模型共享同名材质
|
|
||||||
* @param meshes 网格数组
|
|
||||||
* @param modelId 模型ID
|
|
||||||
*/
|
|
||||||
private cloneMaterials(meshes: AbstractMesh[], modelId: string): void {
|
|
||||||
const scene = this.mainApp.appScene.object;
|
|
||||||
const clonedMaterials = new Map<string, any>();
|
|
||||||
|
|
||||||
meshes.forEach(mesh => {
|
|
||||||
if (mesh.material) {
|
|
||||||
const originalMaterial = mesh.material;
|
|
||||||
const originalName = originalMaterial.name;
|
|
||||||
|
|
||||||
// 如果该材质还没有被克隆过,则克隆它
|
|
||||||
if (!clonedMaterials.has(originalName)) {
|
|
||||||
const newName = `${originalName}_${modelId}`;
|
|
||||||
const clonedMaterial = originalMaterial.clone(newName);
|
|
||||||
clonedMaterials.set(originalName, clonedMaterial);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用克隆的材质
|
|
||||||
mesh.material = clonedMaterials.get(originalName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 为网格设置阴影(投射和接收) */
|
|
||||||
private createModelRoot(modelId: string, meshes: AbstractMesh[]): AbstractMesh[] {
|
|
||||||
const scene = this.mainApp.appScene.object;
|
|
||||||
const root = new Mesh(`${modelId}__root`, scene);
|
|
||||||
const meshSet = new Set<AbstractMesh>(meshes);
|
|
||||||
root.position.copyFrom(this.getMeshesBoundingCenter(meshes));
|
|
||||||
|
|
||||||
meshes.forEach(mesh => {
|
|
||||||
if (!mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)) {
|
|
||||||
mesh.setParent(root, true, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadedMeshes.push(root);
|
|
||||||
return [root, ...meshes];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMeshesBoundingCenter(meshes: AbstractMesh[]): Vector3 {
|
|
||||||
const renderableMeshes = meshes.filter(mesh => !mesh.isDisposed() && mesh.getTotalVertices() > 0);
|
|
||||||
if (!renderableMeshes.length) return Vector3.Zero();
|
|
||||||
|
|
||||||
const min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
|
|
||||||
const max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
|
|
||||||
|
|
||||||
renderableMeshes.forEach(mesh => {
|
|
||||||
mesh.computeWorldMatrix(true);
|
|
||||||
const boundingBox = mesh.getBoundingInfo().boundingBox;
|
|
||||||
min.minimizeInPlace(boundingBox.minimumWorld);
|
|
||||||
max.maximizeInPlace(boundingBox.maximumWorld);
|
|
||||||
});
|
|
||||||
|
|
||||||
return min.add(max).scaleInPlace(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setupShadows(meshes: AbstractMesh[]): void {
|
|
||||||
const appLight = this.mainApp.appLight;
|
|
||||||
if (!appLight) return;
|
|
||||||
|
|
||||||
meshes.forEach(mesh => {
|
|
||||||
if (mesh.getTotalVertices() > 0) {
|
|
||||||
appLight.addShadowCaster(mesh);
|
|
||||||
mesh.receiveShadows = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取缓存的网格 */
|
|
||||||
getCachedMeshes(name: string): AbstractMesh[] | undefined {
|
|
||||||
return this.modelDic.Get(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清理所有资源 */
|
|
||||||
clean(): void {
|
|
||||||
this.modelDic.Clear();
|
|
||||||
this.loadedMeshes.forEach(m => m?.dispose());
|
|
||||||
this.loadedMeshes = [];
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加模型到场景(支持单个或批量)
|
|
||||||
* @param modelConfig 模型配置对象 或 模型配置数组
|
|
||||||
*/
|
|
||||||
async add(
|
|
||||||
modelConfig: ModelMetadata | ModelMetadata[]
|
|
||||||
): Promise<LoadResult | { success: boolean; results: LoadResult[] }> {
|
|
||||||
// 批量加载
|
|
||||||
if (Array.isArray(modelConfig)) {
|
|
||||||
return await this.addMultiple(modelConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 单个加载
|
|
||||||
return await this.addSingle(
|
|
||||||
modelConfig.modelName,
|
|
||||||
modelConfig.modelId,
|
|
||||||
modelConfig.modelUrl,
|
|
||||||
modelConfig.modelControlType,
|
|
||||||
modelConfig.drag,
|
|
||||||
modelConfig.transform
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加单个模型
|
|
||||||
*/
|
|
||||||
private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise<LoadResult> {
|
|
||||||
// 检查是否已存在
|
|
||||||
const existingMeshes = this.modelDic.Get(modelName+'_'+modelId);
|
|
||||||
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
|
||||||
console.log(`模型 ${modelName+modelId} 已存在,直接显示`);
|
|
||||||
this.showMeshes(existingMeshes);
|
|
||||||
return { success: true, meshes: existingMeshes };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载模型
|
|
||||||
const result = await this.loadSingleModel(modelUrl, (event) => {
|
|
||||||
this.emitSingleProgress(modelUrl, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success && result.meshes) {
|
|
||||||
// 克隆材质,确保每个模型有独立的材质
|
|
||||||
this.cloneMaterials(result.meshes, modelId);
|
|
||||||
|
|
||||||
// 存储模型
|
|
||||||
result.meshes = this.createModelRoot(modelName+'_'+modelId, result.meshes);
|
|
||||||
this.modelDic.Set(modelName+'_'+modelId, result.meshes);
|
|
||||||
|
|
||||||
// 存储元数据
|
|
||||||
this.modelMetadataDic.Set(modelName+'_'+modelId, {
|
|
||||||
modelName: modelName,
|
|
||||||
modelId: modelId,
|
|
||||||
modelUrl: modelUrl,
|
|
||||||
modelControlType: modelControlType,
|
|
||||||
drag: drag,
|
|
||||||
transform: transform
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用 transform
|
|
||||||
if (transform) {
|
|
||||||
this.applyTransform(modelId, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置拖拽功能
|
|
||||||
if (drag) {
|
|
||||||
this.mainApp.appModelDrag?.configureDrag(modelId, drag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 GameManager 的字典
|
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
|
||||||
|
|
||||||
EventBridge.modelLoaded({ urls: [modelUrl] });
|
|
||||||
} else {
|
|
||||||
EventBridge.modelLoadError({ url: modelUrl, error: result.error });
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量添加模型
|
|
||||||
*/
|
|
||||||
private async addMultiple(models: ModelMetadata[]): Promise<{ success: boolean; results: LoadResult[] }> {
|
|
||||||
const total = models.length;
|
|
||||||
const results: LoadResult[] = [];
|
|
||||||
|
|
||||||
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
|
|
||||||
|
|
||||||
for (let i = 0; i < models.length; i++) {
|
|
||||||
const { modelName, modelId, modelUrl, modelControlType, drag, transform } = models[i];
|
|
||||||
|
|
||||||
const result = await this.loadSingleModel(modelUrl, (event) => {
|
|
||||||
this.emitProgress(i, total, modelUrl, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success && result.meshes) {
|
|
||||||
// 克隆材质,确保每个模型有独立的材质
|
|
||||||
this.cloneMaterials(result.meshes, modelId);
|
|
||||||
|
|
||||||
result.meshes = this.createModelRoot(modelName, result.meshes);
|
|
||||||
this.modelDic.Set(modelName+'_'+modelId, result.meshes);
|
|
||||||
|
|
||||||
// 存储元数据
|
|
||||||
this.modelMetadataDic.Set(modelName+'_'+modelId, {
|
|
||||||
modelName: modelName,
|
|
||||||
modelId: modelId,
|
|
||||||
modelUrl: modelUrl,
|
|
||||||
modelControlType: modelControlType,
|
|
||||||
drag: drag,
|
|
||||||
transform: transform
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用 transform
|
|
||||||
if (transform) {
|
|
||||||
this.applyTransform(modelName+'_'+modelId, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置拖拽功能
|
|
||||||
if (drag) {
|
|
||||||
this.mainApp.appModelDrag?.configureDrag(modelName+'_'+modelId, drag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(result);
|
|
||||||
this.emitProgress(i + 1, total, modelUrl, null, result.success);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量加载完成后统一更新字典
|
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
|
||||||
|
|
||||||
EventBridge.modelLoaded({ urls: models.map(m => m.modelUrl) });
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: results.every(r => r.success),
|
|
||||||
results
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示网格
|
|
||||||
*/
|
|
||||||
private showMeshes(meshes: AbstractMesh[]): void {
|
|
||||||
meshes.forEach(mesh => {
|
|
||||||
mesh.setEnabled(true);
|
|
||||||
mesh.getChildMeshes().forEach(child => child.setEnabled(true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送单个模型加载进度
|
|
||||||
*/
|
|
||||||
private emitSingleProgress(url: string, event: ISceneLoaderProgressEvent): void {
|
|
||||||
const progress = event.lengthComputable && event.total > 0
|
|
||||||
? Math.min(1, event.loaded / event.total)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
EventBridge.modelLoadProgress({
|
|
||||||
loaded: progress,
|
|
||||||
total: 1,
|
|
||||||
url,
|
|
||||||
progress,
|
|
||||||
percentage: Number((progress * 100).toFixed(2)),
|
|
||||||
detail: {
|
|
||||||
url,
|
|
||||||
lengthComputable: event.lengthComputable,
|
|
||||||
loadedBytes: event.loaded,
|
|
||||||
totalBytes: event.total
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 mesh 名称查找 mesh 对象
|
|
||||||
* @param meshName mesh 名称
|
|
||||||
* @returns mesh 对象,未找到返回 undefined
|
|
||||||
*/
|
|
||||||
private findMeshByName(meshName: string): AbstractMesh | undefined {
|
|
||||||
const keys = this.modelDic.Keys();
|
|
||||||
for (const key of keys) {
|
|
||||||
const meshes = this.modelDic.Get(key);
|
|
||||||
const found = meshes?.find(m => m.name === meshName);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 mesh 查找所属的模型名称
|
|
||||||
* @param mesh 网格对象
|
|
||||||
* @returns 模型名称,未找到返回 undefined
|
|
||||||
*/
|
|
||||||
findModelNameByMesh(mesh: AbstractMesh): string | undefined {
|
|
||||||
|
|
||||||
const keys = this.modelDic.Keys();
|
|
||||||
for (const key of keys) {
|
|
||||||
const meshes = this.modelDic.Get(key);
|
|
||||||
meshes.forEach(mesh => {
|
|
||||||
console.log(mesh.uniqueId);
|
|
||||||
console.log(mesh.name);
|
|
||||||
|
|
||||||
});
|
|
||||||
if (meshes?.some(m => m === mesh || m.uniqueId === mesh.uniqueId)) {
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 mesh 或 mesh 名称移除所属的整个模型
|
|
||||||
* @param meshOrName 网格对象或网格名称
|
|
||||||
* @returns 是否成功移除
|
|
||||||
*/
|
|
||||||
remove(meshOrName: AbstractMesh | string): boolean {
|
|
||||||
let mesh: AbstractMesh | undefined;
|
|
||||||
|
|
||||||
|
|
||||||
// 判断传入的是对象还是字符串
|
|
||||||
if (typeof meshOrName === 'string') {
|
|
||||||
mesh = this.findMeshByName(meshOrName);
|
|
||||||
if (!mesh) {
|
|
||||||
console.warn(`未找到名为 ${meshOrName} 的网格`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mesh = meshOrName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelName = this.findModelNameByMesh(mesh);
|
|
||||||
if (modelName) {
|
|
||||||
this.removeByName(modelName);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
console.warn('未找到该 mesh 所属的模型');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 替换模型
|
|
||||||
* @param modelConfig 模型配置对象
|
|
||||||
*/
|
|
||||||
async replaceModel(modelConfig: ModelMetadata): Promise<LoadResult> {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.removeByName(modelConfig.modelId);
|
|
||||||
return await this.addSingle(
|
|
||||||
modelConfig.modelName,
|
|
||||||
modelConfig.modelId,
|
|
||||||
modelConfig.modelUrl,
|
|
||||||
modelConfig.modelControlType,
|
|
||||||
modelConfig.drag,
|
|
||||||
modelConfig.transform
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁指定模型
|
|
||||||
* @param modelName 模型名称
|
|
||||||
*/
|
|
||||||
removeByName(modelName: string): void {
|
|
||||||
const meshes = this.modelDic.Get(modelName);
|
|
||||||
if (!meshes?.length) {
|
|
||||||
console.warn(`Model not found: ${modelName}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true));
|
|
||||||
this.modelDic.Remove(modelName);
|
|
||||||
this.modelMetadataDic.Remove(modelName);
|
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除所有已添加的模型并释放内存
|
|
||||||
* 主要用于切换尺寸后清除不适用的配件
|
|
||||||
*/
|
|
||||||
removeAll(): void {
|
|
||||||
const modelNames = this.modelDic.Keys();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
modelNames.forEach(modelName => {
|
|
||||||
const meshes = this.modelDic.Get(modelName);
|
|
||||||
if (meshes?.length) {
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modelDic.Clear();
|
|
||||||
this.modelMetadataDic.Clear();
|
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
|
||||||
|
|
||||||
console.log('所有模型已清除,内存已释放');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取模型元数据
|
|
||||||
* @param modelName 模型名称
|
|
||||||
*/
|
|
||||||
getModelMetadata(modelName: string): ModelMetadata | undefined {
|
|
||||||
return this.modelMetadataDic.Get(modelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据网格查找模型元数据
|
|
||||||
* @param mesh 网格对象
|
|
||||||
*/
|
|
||||||
getMetadataByMesh(mesh: AbstractMesh): ModelMetadata | undefined {
|
|
||||||
const modelName = this.findModelNameByMesh(mesh);
|
|
||||||
if (modelName) {
|
|
||||||
return this.modelMetadataDic.Get(modelName);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getModelTransformTargets(meshes: AbstractMesh[]): AbstractMesh[] {
|
|
||||||
const meshSet = new Set<AbstractMesh>(meshes);
|
|
||||||
const rootMeshes = meshes.filter(mesh => !mesh.parent || !meshSet.has(mesh.parent as AbstractMesh));
|
|
||||||
|
|
||||||
return rootMeshes.length ? rootMeshes : meshes.slice(0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
getModelTransformTargetByMesh(mesh: AbstractMesh): AbstractMesh | undefined {
|
|
||||||
const modelName = this.findModelNameByMesh(mesh);
|
|
||||||
if (!modelName) return mesh;
|
|
||||||
|
|
||||||
const meshes = this.modelDic.Get(modelName);
|
|
||||||
if (!meshes?.length) return mesh;
|
|
||||||
|
|
||||||
return this.getModelTransformTargets(meshes)[0] ?? mesh;
|
|
||||||
}
|
|
||||||
|
|
||||||
getModelMeshesByMesh(mesh: AbstractMesh): AbstractMesh[] {
|
|
||||||
const modelName = this.findModelNameByMesh(mesh);
|
|
||||||
if (!modelName) return [mesh];
|
|
||||||
|
|
||||||
const meshes = this.modelDic.Get(modelName);
|
|
||||||
return meshes?.length ? meshes : [mesh];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置模型旋转
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @param rotation 旋转向量 {x, y, z}(默认使用角度)
|
|
||||||
* @param useDegrees 是否使用角度(默认true)
|
|
||||||
*/
|
|
||||||
setRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void {
|
|
||||||
const meshes = this.modelDic.Get(modelId);
|
|
||||||
if (!meshes?.length) {
|
|
||||||
console.warn(`Model not found: ${modelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果使用角度,转换为弧度
|
|
||||||
const toRadians = (degrees: number) => degrees * Math.PI / 180;
|
|
||||||
const rotationValues = useDegrees ? {
|
|
||||||
x: toRadians(rotation.x),
|
|
||||||
y: toRadians(rotation.y),
|
|
||||||
z: toRadians(rotation.z)
|
|
||||||
} : rotation;
|
|
||||||
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => {
|
|
||||||
if (mesh.rotationQuaternion) {
|
|
||||||
mesh.rotationQuaternion = Quaternion.FromEulerAngles(
|
|
||||||
rotationValues.x,
|
|
||||||
rotationValues.y,
|
|
||||||
rotationValues.z
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mesh.rotation.set(rotationValues.x, rotationValues.y, rotationValues.z);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 累加模型旋转
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @param rotation 旋转向量 {x, y, z}(默认使用角度)
|
|
||||||
* @param useDegrees 是否使用角度(默认true)
|
|
||||||
*/
|
|
||||||
addRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void {
|
|
||||||
const meshes = this.modelDic.Get(modelId);
|
|
||||||
if (!meshes?.length) {
|
|
||||||
console.warn(`Model not found: ${modelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果使用角度,转换为弧度
|
|
||||||
const toRadians = (degrees: number) => degrees * Math.PI / 180;
|
|
||||||
const rotationValues = useDegrees ? {
|
|
||||||
x: toRadians(rotation.x),
|
|
||||||
y: toRadians(rotation.y),
|
|
||||||
z: toRadians(rotation.z)
|
|
||||||
} : rotation;
|
|
||||||
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => {
|
|
||||||
mesh.addRotation(rotationValues.x, rotationValues.y, rotationValues.z);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置模型位置
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @param position 位置向量 {x, y, z}
|
|
||||||
*/
|
|
||||||
setPosition(modelId: string, position: { x: number; y: number; z: number }): void {
|
|
||||||
const meshes = this.modelDic.Get(modelId);
|
|
||||||
if (!meshes?.length) {
|
|
||||||
console.warn(`Model not found: ${modelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => {
|
|
||||||
mesh.position.x = position.x;
|
|
||||||
mesh.position.y = position.y;
|
|
||||||
mesh.position.z = position.z;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置模型缩放
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @param scale 缩放向量 {x, y, z}
|
|
||||||
*/
|
|
||||||
setScale(modelId: string, scale: { x: number; y: number; z: number }): void {
|
|
||||||
const meshes = this.modelDic.Get(modelId);
|
|
||||||
if (!meshes?.length) {
|
|
||||||
console.warn(`Model not found: ${modelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => {
|
|
||||||
mesh.scaling.x = scale.x;
|
|
||||||
mesh.scaling.y = scale.y;
|
|
||||||
mesh.scaling.z = scale.z;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将模型放置到指定的放置区域
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @param zoneInfo 放置区域信息
|
|
||||||
* @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外)
|
|
||||||
*/
|
|
||||||
placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void {
|
|
||||||
const meshes = this.modelDic.Get(modelId);
|
|
||||||
if (!meshes?.length) {
|
|
||||||
console.warn(`Model not found: ${modelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算放置位置:中心点 + 法线方向的偏移
|
|
||||||
const targetPosition = zoneInfo.center.add(zoneInfo.normal.scale(offsetDistance));
|
|
||||||
|
|
||||||
// 计算旋转角度:让模型面向墙面(法线的反方向)
|
|
||||||
const targetDirection = zoneInfo.normal.scale(-1);
|
|
||||||
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
|
||||||
|
|
||||||
this.getModelTransformTargets(meshes).forEach(mesh => {
|
|
||||||
// 设置位置
|
|
||||||
mesh.position.copyFrom(targetPosition);
|
|
||||||
|
|
||||||
// 设置旋转(只旋转Y轴,让模型面向墙面)
|
|
||||||
if (mesh.rotationQuaternion) {
|
|
||||||
mesh.rotationQuaternion = Quaternion.FromEulerAngles(0, angle, 0);
|
|
||||||
} else {
|
|
||||||
mesh.rotation.set(0, angle, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查模型是否存在
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @returns 模型是否存在
|
|
||||||
*/
|
|
||||||
exists(modelId: string): boolean {
|
|
||||||
return this.modelDic.Has(modelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用 transform 到模型
|
|
||||||
* @param modelId 模型ID
|
|
||||||
* @param transform 变换信息
|
|
||||||
*/
|
|
||||||
private applyTransform(modelId: string, transform: ModelTransform): void {
|
|
||||||
// 应用位置
|
|
||||||
if (transform.position) {
|
|
||||||
this.setPosition(modelId, transform.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用旋转(角度制)
|
|
||||||
if (transform.rotation) {
|
|
||||||
this.setRotation(modelId, transform.rotation, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用缩放
|
|
||||||
if (transform.scale) {
|
|
||||||
this.setScale(modelId, transform.scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -215,18 +215,6 @@ export class AppModel extends Monobehiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setupShadows(meshes: AbstractMesh[]): void {
|
|
||||||
const appLight = this.mainApp.appLight;
|
|
||||||
if (!appLight) return;
|
|
||||||
|
|
||||||
meshes.forEach(mesh => {
|
|
||||||
if (mesh.getTotalVertices() > 0) {
|
|
||||||
appLight.addShadowCaster(mesh);
|
|
||||||
mesh.receiveShadows = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取缓存的网格 */
|
/** 获取缓存的网格 */
|
||||||
getCachedMeshes(name: string): AbstractMesh[] | undefined {
|
getCachedMeshes(name: string): AbstractMesh[] | undefined {
|
||||||
return this.modelDic.Get(name);
|
return this.modelDic.Get(name);
|
||||||
@ -729,9 +717,10 @@ export class AppModel extends Monobehiver {
|
|||||||
* 将模型放置到指定的放置区域
|
* 将模型放置到指定的放置区域
|
||||||
* @param modelId 模型ID
|
* @param modelId 模型ID
|
||||||
* @param zoneInfo 放置区域信息
|
* @param zoneInfo 放置区域信息
|
||||||
* @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外)
|
* @param offsetDistance 距离墙面的偏移距离(默认0,正数向外)
|
||||||
*/
|
*/
|
||||||
placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void {
|
placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void {
|
||||||
|
console.log(zoneInfo);
|
||||||
const meshes = this.modelDic.Get(modelId);
|
const meshes = this.modelDic.Get(modelId);
|
||||||
if (!meshes?.length) {
|
if (!meshes?.length) {
|
||||||
console.warn(`Model not found: ${modelId}`);
|
console.warn(`Model not found: ${modelId}`);
|
||||||
|
|||||||
@ -11,6 +11,10 @@ export interface DragConfig {
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
|
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
|
||||||
step?: number;
|
step?: number;
|
||||||
|
snapToZone?: boolean; // 拖拽吸附:松开时自动吸附到最近的分割区域
|
||||||
|
returnWhenOutOfBounds?: boolean; // 拖拽到区域外时返回原位置
|
||||||
|
handleOccupiedZone?: boolean; // 拖拽到已占用区域时的处理:true=返回原位置或替换,false=允许重叠
|
||||||
|
occupiedZoneAction?: 'return' | 'replace'; // 当 handleOccupiedZone=true 时的具体行为:'return' 返回原位置,'replace' 替换目标位置的模型
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,16 +115,68 @@ export class AppModelDrag extends Monobehiver {
|
|||||||
// 使用世界坐标系而不是物体本地坐标系
|
// 使用世界坐标系而不是物体本地坐标系
|
||||||
pointerDragBehavior.useObjectOrientationForDragging = false;
|
pointerDragBehavior.useObjectOrientationForDragging = false;
|
||||||
|
|
||||||
|
// 记录拖拽起始位置和状态
|
||||||
|
let dragStartPosition: Vector3 | null = null;
|
||||||
|
let hasShownZones = false; // 是否已显示分割区域
|
||||||
|
|
||||||
// 监听拖拽开始事件
|
// 监听拖拽开始事件
|
||||||
pointerDragBehavior.onDragStartObservable.add(() => {
|
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||||
|
// 记录起始位置
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (meshes && meshes.length > 0) {
|
||||||
|
dragStartPosition = meshes[0].position.clone();
|
||||||
|
}
|
||||||
|
|
||||||
// 禁用相机控制
|
// 禁用相机控制
|
||||||
this.disableCameraControl();
|
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(() => {
|
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; // 移动距离大于 0.01 才算拖拽
|
||||||
|
}
|
||||||
|
|
||||||
// 恢复相机控制
|
// 恢复相机控制
|
||||||
this.enableCameraControl();
|
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;
|
return pointerDragBehavior;
|
||||||
@ -239,6 +295,339 @@ export class AppModelDrag extends Monobehiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示模型所在墙面的分割区域
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private showZonesForModel(modelId: string): void {
|
||||||
|
const appDropZone = this.mainApp.appDropZone;
|
||||||
|
if (!appDropZone) return;
|
||||||
|
|
||||||
|
// 查找模型所在的墙面
|
||||||
|
let wallName: string | null = null;
|
||||||
|
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
|
||||||
|
if (id === modelId) {
|
||||||
|
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
|
||||||
|
if (match) {
|
||||||
|
wallName = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wallName) {
|
||||||
|
console.log(`[拖拽吸附] 显示墙面 ${wallName} 的分割区域`);
|
||||||
|
// 只显示该墙面的分割区域
|
||||||
|
appDropZone.showWall(wallName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏分割区域
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private hideZonesForModel(modelId: string): void {
|
||||||
|
const appDropZone = this.mainApp.appDropZone;
|
||||||
|
if (!appDropZone) return;
|
||||||
|
|
||||||
|
|
||||||
|
appDropZone.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将模型吸附到最近的分割区域
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private snapModelToZone(modelId: string): void {
|
||||||
|
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: string | null = null;
|
||||||
|
let originalZoneIndex: number = -1;
|
||||||
|
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
|
||||||
|
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) {
|
||||||
|
// 左右墙面(法线沿X轴)
|
||||||
|
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) {
|
||||||
|
// 前后墙面(法线沿Z轴)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理超出边界的情况(开关2:returnWhenOutOfBounds)
|
||||||
|
if (isOutOfBounds) {
|
||||||
|
|
||||||
|
|
||||||
|
if (returnWhenOutOfBounds) {
|
||||||
|
// 启用了边界返回,回到原来的区域
|
||||||
|
if (originalZoneIndex !== -1) {
|
||||||
|
const originalZone = wallZones[originalZoneIndex];
|
||||||
|
if (originalZone) {
|
||||||
|
const offsetDistance = 0;
|
||||||
|
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance));
|
||||||
|
rootMesh.position.copyFrom(returnPosition);
|
||||||
|
|
||||||
|
const targetDirection = originalZone.normal.scale(-1);
|
||||||
|
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||||
|
rootMesh.rotation.y = angle;
|
||||||
|
|
||||||
|
return; // 不更新映射,保持原映射
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 未启用边界返回,保持当前位置,不做吸附
|
||||||
|
|
||||||
|
// 更新映射关系(可能移出了原区域)
|
||||||
|
this.updateModelZoneMapping(modelId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetZone = wallZones[closestZoneIndex];
|
||||||
|
|
||||||
|
// 检查目标区域是否已被其他模型占用(开关2:handleOccupiedZone)
|
||||||
|
const targetZoneKey = `${wallName}[${closestZoneIndex}]`;
|
||||||
|
const occupyingModelId = appDropZone['zoneModelMap']?.get(targetZoneKey);
|
||||||
|
|
||||||
|
if (occupyingModelId && occupyingModelId !== modelId) {
|
||||||
|
// 目标区域已被其他模型占用
|
||||||
|
|
||||||
|
|
||||||
|
if (handleOccupiedZone) {
|
||||||
|
// 启用了占用区域处理
|
||||||
|
if (occupiedZoneAction === 'return') {
|
||||||
|
// 返回原位置
|
||||||
|
|
||||||
|
if (originalZoneIndex !== -1) {
|
||||||
|
const originalZone = wallZones[originalZoneIndex];
|
||||||
|
if (originalZone) {
|
||||||
|
const offsetDistance = 0;
|
||||||
|
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance));
|
||||||
|
rootMesh.position.copyFrom(returnPosition);
|
||||||
|
|
||||||
|
const targetDirection = originalZone.normal.scale(-1);
|
||||||
|
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||||
|
rootMesh.rotation.y = angle;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
// 更新映射关系
|
||||||
|
this.updateModelZoneMapping(modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新模型所属的分割区域映射
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private updateModelZoneMapping(modelId: string): void {
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (!meshes || !meshes.length) return;
|
||||||
|
|
||||||
|
const rootMesh = meshes[0];
|
||||||
|
const modelPosition = rootMesh.position;
|
||||||
|
|
||||||
|
|
||||||
|
// 获取 AppDropZone
|
||||||
|
const appDropZone = this.mainApp.appDropZone;
|
||||||
|
if (!appDropZone) return;
|
||||||
|
|
||||||
|
// 查找该模型原本所在的墙面
|
||||||
|
let originalWallName: string | null = null;
|
||||||
|
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
|
||||||
|
if (id === modelId) {
|
||||||
|
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
|
||||||
|
if (match) {
|
||||||
|
originalWallName = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!originalWallName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取该墙面的所有分割区域
|
||||||
|
const wallZones = appDropZone.getZonesByWall(originalWallName);
|
||||||
|
if (!wallZones.length) return;
|
||||||
|
|
||||||
|
|
||||||
|
// 计算模型与每个分割区域的距离,找到最近的区域
|
||||||
|
let closestZoneIndex = -1;
|
||||||
|
let minDistance = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
wallZones.forEach((zone, index) => {
|
||||||
|
// 计算模型位置到区域中心的距离
|
||||||
|
const distance = modelPosition.subtract(zone.center).length();
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestZoneIndex = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (closestZoneIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 查找模型当前所在的区域索引
|
||||||
|
let currentZoneIndex = -1;
|
||||||
|
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
|
||||||
|
if (id === modelId) {
|
||||||
|
const match = zoneKey.match(/^.+\[(\d+)\]$/);
|
||||||
|
if (match) {
|
||||||
|
currentZoneIndex = parseInt(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果模型移动到了新的区域,更新映射
|
||||||
|
if (currentZoneIndex !== closestZoneIndex) {
|
||||||
|
|
||||||
|
// 删除旧映射
|
||||||
|
if (currentZoneIndex !== -1) {
|
||||||
|
const oldKey = `${originalWallName}[${currentZoneIndex}]`;
|
||||||
|
appDropZone['zoneModelMap']?.delete(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) {
|
||||||
|
|
||||||
|
// 只有在启用占用区域处理且为 'replace' 模式下才交换位置
|
||||||
|
if (handleOccupiedZone && occupiedZoneAction === 'replace') {
|
||||||
|
|
||||||
|
// 将原有模型移动到旧位置
|
||||||
|
if (currentZoneIndex !== -1) {
|
||||||
|
const swapKey = `${originalWallName}[${currentZoneIndex}]`;
|
||||||
|
appDropZone['zoneModelMap']?.set(swapKey, existingModelId);
|
||||||
|
|
||||||
|
// 实际移动被替换模型的物理位置
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新映射
|
||||||
|
appDropZone['zoneModelMap']?.set(newKey, modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理资源
|
* 清理资源
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export interface PlacementZoneInfo {
|
|||||||
|
|
||||||
export class AppPlacementWall {
|
export class AppPlacementWall {
|
||||||
private scene: Scene;
|
private scene: Scene;
|
||||||
private placementZones: PlacementZoneInfo[] = [];
|
private placementZones: Map<string, PlacementZoneInfo[]> = new Map();
|
||||||
private borderLines: Mesh[] = [];
|
private borderLines: Mesh[] = [];
|
||||||
private onZoneClickCallback?: (zoneInfo: PlacementZoneInfo) => void;
|
private onZoneClickCallback?: (zoneInfo: PlacementZoneInfo) => void;
|
||||||
|
|
||||||
@ -60,21 +60,31 @@ export class AppPlacementWall {
|
|||||||
borderColor = '#ffffff'
|
borderColor = '#ffffff'
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
// 清除之前的放置区域
|
// 不再清除所有区域,只清除和更新本次传入的墙面
|
||||||
this.clearAll();
|
|
||||||
|
|
||||||
const material = this.createMaterial(color, alpha);
|
const material = this.createMaterial(color, alpha);
|
||||||
|
const allZones: PlacementZoneInfo[] = [];
|
||||||
|
|
||||||
walls.forEach(wall => {
|
walls.forEach(wall => {
|
||||||
|
// 先清除该墙面的旧数据(dispose 旧 mesh)
|
||||||
|
const oldZones = this.placementZones.get(wall.name);
|
||||||
|
if (oldZones) {
|
||||||
|
oldZones.forEach(zone => zone.mesh.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除该墙面的旧边框
|
||||||
|
this.clearWallBorders(wall.name);
|
||||||
|
|
||||||
|
// 生成新的区域
|
||||||
const zones = this.generateWallZones(wall, material, thickness);
|
const zones = this.generateWallZones(wall, material, thickness);
|
||||||
this.placementZones.push(...zones);
|
this.placementZones.set(wall.name, zones);
|
||||||
|
allZones.push(...zones);
|
||||||
|
|
||||||
if (showBorder) {
|
if (showBorder) {
|
||||||
this.createWallBorder(wall, borderColor);
|
this.createWallBorder(wall, borderColor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.placementZones;
|
return allZones;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -302,23 +312,26 @@ export class AppPlacementWall {
|
|||||||
* 获取所有放置区域
|
* 获取所有放置区域
|
||||||
*/
|
*/
|
||||||
getPlacementZones(): PlacementZoneInfo[] {
|
getPlacementZones(): PlacementZoneInfo[] {
|
||||||
return this.placementZones;
|
const allZones: PlacementZoneInfo[] = [];
|
||||||
|
this.placementZones.forEach(zones => {
|
||||||
|
allZones.push(...zones);
|
||||||
|
});
|
||||||
|
return allZones;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据墙面名称获取放置区域
|
* 根据墙面名称获取放置区域
|
||||||
*/
|
*/
|
||||||
getZonesByWall(wallName: string): PlacementZoneInfo[] {
|
getZonesByWall(wallName: string): PlacementZoneInfo[] {
|
||||||
return this.placementZones.filter(zone => zone.wallName === wallName);
|
return this.placementZones.get(wallName) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据索引获取特定放置区域
|
* 根据索引获取特定放置区域
|
||||||
*/
|
*/
|
||||||
getZone(wallName: string, index: number): PlacementZoneInfo | undefined {
|
getZone(wallName: string, index: number): PlacementZoneInfo | undefined {
|
||||||
return this.placementZones.find(
|
const zones = this.placementZones.get(wallName);
|
||||||
zone => zone.wallName === wallName && zone.index === index
|
return zones?.find(zone => zone.index === index);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -332,34 +345,78 @@ export class AppPlacementWall {
|
|||||||
* 显示所有放置区域
|
* 显示所有放置区域
|
||||||
*/
|
*/
|
||||||
show(): void {
|
show(): void {
|
||||||
this.placementZones.forEach(zone => {
|
this.placementZones.forEach(zones => {
|
||||||
|
zones.forEach(zone => {
|
||||||
zone.mesh.isVisible = true;
|
zone.mesh.isVisible = true;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
this.borderLines.forEach(line => {
|
this.borderLines.forEach(line => {
|
||||||
line.isVisible = true;
|
line.isVisible = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只显示指定墙面的放置区域
|
||||||
|
* @param wallName 墙面名称
|
||||||
|
*/
|
||||||
|
showWall(wallName: string): void {
|
||||||
|
// 不隐藏其他墙面,只显示指定墙面
|
||||||
|
// 只显示指定墙面的区域
|
||||||
|
const zones = this.placementZones.get(wallName);
|
||||||
|
if (zones) {
|
||||||
|
zones.forEach(zone => {
|
||||||
|
zone.mesh.isVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示该墙面的边框(根据名称过滤)
|
||||||
|
this.borderLines.forEach(line => {
|
||||||
|
// 边框线名称格式:block_border_${wallName}_${index}_${edgeIndex} 或 border_${wallName}_${index}
|
||||||
|
if (line.name.includes(`_${wallName}_`)) {
|
||||||
|
line.isVisible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 隐藏所有放置区域
|
* 隐藏所有放置区域
|
||||||
*/
|
*/
|
||||||
hide(): void {
|
hide(): void {
|
||||||
this.placementZones.forEach(zone => {
|
this.placementZones.forEach(zones => {
|
||||||
|
zones.forEach(zone => {
|
||||||
zone.mesh.isVisible = false;
|
zone.mesh.isVisible = false;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
this.borderLines.forEach(line => {
|
this.borderLines.forEach(line => {
|
||||||
line.isVisible = false;
|
line.isVisible = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除指定墙面的边框
|
||||||
|
*/
|
||||||
|
private clearWallBorders(wallName: string): void {
|
||||||
|
const linesToRemove: Mesh[] = [];
|
||||||
|
this.borderLines.forEach(line => {
|
||||||
|
if (line.name.includes(`_${wallName}_`)) {
|
||||||
|
line.dispose();
|
||||||
|
linesToRemove.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 从数组中移除已清除的边框
|
||||||
|
this.borderLines = this.borderLines.filter(line => !linesToRemove.includes(line));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除所有放置区域
|
* 清除所有放置区域
|
||||||
*/
|
*/
|
||||||
clearAll(): void {
|
clearAll(): void {
|
||||||
this.placementZones.forEach(zone => {
|
this.placementZones.forEach(zones => {
|
||||||
|
zones.forEach(zone => {
|
||||||
zone.mesh.dispose();
|
zone.mesh.dispose();
|
||||||
});
|
});
|
||||||
this.placementZones = [];
|
});
|
||||||
|
this.placementZones.clear();
|
||||||
|
|
||||||
this.borderLines.forEach(line => {
|
this.borderLines.forEach(line => {
|
||||||
line.dispose();
|
line.dispose();
|
||||||
|
|||||||
@ -21,6 +21,11 @@ class AppRay extends Monobehiver {
|
|||||||
private highlightLayer: HighlightLayer | null = null
|
private highlightLayer: HighlightLayer | null = null
|
||||||
private originalMaterial: any = null
|
private originalMaterial: any = null
|
||||||
private highlightedMesh: AbstractMesh | null = null
|
private highlightedMesh: AbstractMesh | null = null
|
||||||
|
private pointerDownTime: number = 0
|
||||||
|
private pointerDownPickInfo: PickingInfo | null = null
|
||||||
|
private longPressTimer: any = null
|
||||||
|
private longPressThreshold: number = 500 // 长按阈值(毫秒)
|
||||||
|
private isLongPress: boolean = false
|
||||||
|
|
||||||
constructor(mainApp: MainApp) {
|
constructor(mainApp: MainApp) {
|
||||||
super(mainApp)
|
super(mainApp)
|
||||||
@ -58,18 +63,97 @@ class AppRay extends Monobehiver {
|
|||||||
|
|
||||||
if (type === PointerEventTypes.POINTERDOWN) {
|
if (type === PointerEventTypes.POINTERDOWN) {
|
||||||
this.oldPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
|
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) {
|
} else if (type === PointerEventTypes.POINTERUP) {
|
||||||
|
// 清除长按定时器
|
||||||
|
if (this.longPressTimer) {
|
||||||
|
clearTimeout(this.longPressTimer);
|
||||||
|
this.longPressTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
|
this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
|
||||||
const distance = Vector3.Distance(this.oldPoint, this.newPoint);
|
const distance = Vector3.Distance(this.oldPoint, this.newPoint);
|
||||||
|
|
||||||
// 只有在没有移动的情况下才处理单击
|
// 如果是长按后松手,隐藏分割区域,并回退配置
|
||||||
if (distance < 5) { // 增加一些容差
|
if (this.isLongPress) {
|
||||||
|
console.log('[长按] 松手,隐藏分割区域');
|
||||||
|
this.mainApp.appDropZone.hide(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在没有移动且不是长按的情况下才处理单击
|
||||||
|
if (distance < 5 && !this.isLongPress) {
|
||||||
this.handleSingleClick(pointerEvent, pickInfo);
|
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: IPointerEvent, pickInfo: PickingInfo | null) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找模型所在的墙面
|
||||||
|
private findModelWallName(modelId: string): string | null {
|
||||||
|
const zoneModelMap = this.mainApp.appDropZone['zoneModelMap'] as Map<string, string>;
|
||||||
|
if (!zoneModelMap) return null;
|
||||||
|
|
||||||
|
for (const [zoneKey, id] of zoneModelMap.entries()) {
|
||||||
|
if (id === modelId) {
|
||||||
|
// zoneKey 格式为 "wallName[index]"
|
||||||
|
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 处理单击
|
// 处理单击
|
||||||
handleSingleClick(evt: IPointerEvent, pickInfo: PickingInfo | null) {
|
handleSingleClick(evt: IPointerEvent, pickInfo: PickingInfo | null) {
|
||||||
// 先尝试热点(mesh 热点 / sprite 热点)
|
// 先尝试热点(mesh 热点 / sprite 热点)
|
||||||
@ -93,7 +177,7 @@ class AppRay extends Monobehiver {
|
|||||||
const clickedZone = zones.find(zone => zone.mesh === pickInfo.pickedMesh);
|
const clickedZone = zones.find(zone => zone.mesh === pickInfo.pickedMesh);
|
||||||
if (clickedZone) {
|
if (clickedZone) {
|
||||||
// 计算该放置区域的目标位置和旋转
|
// 计算该放置区域的目标位置和旋转
|
||||||
const offsetDistance = 0.05; // 增加偏移距离,让模型更往外
|
const offsetDistance = 0;
|
||||||
const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance));
|
const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance));
|
||||||
const targetDirection = clickedZone.normal.scale(-1);
|
const targetDirection = clickedZone.normal.scale(-1);
|
||||||
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||||
@ -114,7 +198,7 @@ class AppRay extends Monobehiver {
|
|||||||
},
|
},
|
||||||
rotation: {
|
rotation: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: angle * 180 / Math.PI, // 转换为角度
|
y: angle * 180 / Math.PI,
|
||||||
z: 0
|
z: 0
|
||||||
},
|
},
|
||||||
scale: {
|
scale: {
|
||||||
@ -130,6 +214,9 @@ class AppRay extends Monobehiver {
|
|||||||
|
|
||||||
this.mainApp.appDomTo3D.hideAll()
|
this.mainApp.appDomTo3D.hideAll()
|
||||||
|
|
||||||
|
// 隐藏放置区域,避免遮挡配件模型的点击
|
||||||
|
this.mainApp.appDropZone.hide();
|
||||||
|
|
||||||
const materialName = pickInfo.pickedMesh.material?.name || '';
|
const materialName = pickInfo.pickedMesh.material?.name || '';
|
||||||
const holdingShift = Boolean((evt as any).shiftKey);
|
const holdingShift = Boolean((evt as any).shiftKey);
|
||||||
const modelMeshes = this.mainApp.appModel.getModelMeshesByMesh(pickInfo.pickedMesh);
|
const modelMeshes = this.mainApp.appModel.getModelMeshesByMesh(pickInfo.pickedMesh);
|
||||||
@ -162,8 +249,8 @@ class AppRay extends Monobehiver {
|
|||||||
this.mainApp.appPositionGizmo.detach();
|
this.mainApp.appPositionGizmo.detach();
|
||||||
this.mainApp.appDomTo3D.hideAll();
|
this.mainApp.appDomTo3D.hideAll();
|
||||||
|
|
||||||
// 隐藏放置区域
|
// 隐藏放置区域,并回退到备份配置
|
||||||
this.mainApp.appDropZone?.hide();
|
this.mainApp.appDropZone?.hide(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,5 +19,6 @@ export class AppScene extends Monobehiver {
|
|||||||
this.object = new Scene(this.mainApp.appEngin.object);
|
this.object = new Scene(this.mainApp.appEngin.object);
|
||||||
this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景
|
this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景
|
||||||
this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能
|
this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
349
src/babylonjs/GameManager.full.ts
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3 } from "@babylonjs/core";
|
||||||
|
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||||
|
import { Nullable } from "@babylonjs/core/types";
|
||||||
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
|
import { Dictionary } from '../utils/Dictionary';
|
||||||
|
|
||||||
|
type RollerDoorOptions = {
|
||||||
|
upY?: number;
|
||||||
|
downY?: number;
|
||||||
|
speed?: number;
|
||||||
|
meshNames?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏管理器类 - 负责材质管理和场景控制
|
||||||
|
*
|
||||||
|
* 核心功能:
|
||||||
|
* - 材质管理(applyMaterial)
|
||||||
|
* - 卷帘门动画(待确认是否实际使用)
|
||||||
|
* - Y轴剖切(待确认是否实际使用)
|
||||||
|
*/
|
||||||
|
export class GameManager extends Monobehiver {
|
||||||
|
private materialDic: Dictionary<PBRMaterial>;
|
||||||
|
private meshDic: Dictionary<any>;
|
||||||
|
|
||||||
|
// 卷帘门相关(如未使用可删除)
|
||||||
|
private rollerDoorMeshes: AbstractMesh[];
|
||||||
|
private rollerDoorGroup: AbstractMesh | null;
|
||||||
|
private rollerDoorInitialY: Map<string, number>;
|
||||||
|
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
||||||
|
private rollerDoorIsOpen: boolean;
|
||||||
|
private rollerDoorNames: string[];
|
||||||
|
|
||||||
|
// Y轴剖切相关(如未使用可删除)
|
||||||
|
private yClipPlane: Plane | null;
|
||||||
|
private yClipTargets: string[] | null;
|
||||||
|
|
||||||
|
constructor(mainApp: any) {
|
||||||
|
super(mainApp);
|
||||||
|
this.materialDic = new Dictionary<PBRMaterial>();
|
||||||
|
this.meshDic = new Dictionary<any>();
|
||||||
|
this.rollerDoorMeshes = [];
|
||||||
|
this.rollerDoorGroup = null;
|
||||||
|
this.rollerDoorInitialY = new Map();
|
||||||
|
this.rollerDoorObserver = null;
|
||||||
|
this.rollerDoorIsOpen = false;
|
||||||
|
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
||||||
|
this.yClipPlane = null;
|
||||||
|
this.yClipTargets = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 调试:返回当前场景中所有网格名称 */
|
||||||
|
listMeshNames(): string[] {
|
||||||
|
return this.meshDic.Keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化游戏管理器 */
|
||||||
|
async Awake() {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) {
|
||||||
|
console.warn('Scene not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDictionaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新材质和网格字典(从场景中同步)
|
||||||
|
*/
|
||||||
|
updateDictionaries(): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
this.materialDic.Clear();
|
||||||
|
this.meshDic.Clear();
|
||||||
|
|
||||||
|
// 更新材质字典
|
||||||
|
for (const mat of scene.materials) {
|
||||||
|
if (!(mat as any)._isDisposed && !this.materialDic.Has(mat.name)) {
|
||||||
|
this.materialDic.Set(mat.name, mat as PBRMaterial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新网格字典
|
||||||
|
for (const mesh of scene.meshes) {
|
||||||
|
if (mesh instanceof Mesh && !mesh.isDisposed() && !this.meshDic.Has(mesh.name)) {
|
||||||
|
this.meshDic.Set(mesh.name, mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mat = mesh.material;
|
||||||
|
if (mat instanceof PBRMaterial && !(mat as any)._isDisposed) {
|
||||||
|
this.materialDic.Set(mat.name, mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用材质属性
|
||||||
|
* @param options 材质配置选项
|
||||||
|
*/
|
||||||
|
applyMaterial(options: {
|
||||||
|
target: string;
|
||||||
|
modelId?: string;
|
||||||
|
albedoColor?: string;
|
||||||
|
albedoTexture?: string;
|
||||||
|
normalMap?: string;
|
||||||
|
metallicTexture?: string;
|
||||||
|
roughness?: number;
|
||||||
|
metallic?: number;
|
||||||
|
}): void {
|
||||||
|
this.updateDictionaries();
|
||||||
|
|
||||||
|
const targetMaterials: PBRMaterial[] = [];
|
||||||
|
|
||||||
|
// 如果提供了 modelId,只查找该模型的材质
|
||||||
|
if (options.modelId) {
|
||||||
|
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
||||||
|
|
||||||
|
if (!modelMeshes || modelMeshes.length === 0) {
|
||||||
|
console.warn(`Model not found: ${options.modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modelMeshes.forEach((mesh: AbstractMesh) => {
|
||||||
|
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
||||||
|
const material = mesh.material as PBRMaterial;
|
||||||
|
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||||
|
if (!targetMaterials.includes(material)) {
|
||||||
|
targetMaterials.push(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 没有提供 modelId,全局查找
|
||||||
|
this.materialDic.Values().forEach(material => {
|
||||||
|
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||||
|
targetMaterials.push(material);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetMaterials.length === 0) {
|
||||||
|
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用材质属性
|
||||||
|
targetMaterials.forEach(material => {
|
||||||
|
// 应用颜色
|
||||||
|
if (options.albedoColor) {
|
||||||
|
const color = Color3.FromHexString(options.albedoColor);
|
||||||
|
material.albedoColor.copyFrom(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用反照率纹理
|
||||||
|
if (options.albedoTexture !== undefined) {
|
||||||
|
if (options.albedoTexture) {
|
||||||
|
material.albedoTexture = new Texture(options.albedoTexture, this.mainApp.appScene.object);
|
||||||
|
} else {
|
||||||
|
material.albedoTexture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用法线贴图
|
||||||
|
if (options.normalMap !== undefined) {
|
||||||
|
if (options.normalMap) {
|
||||||
|
material.bumpTexture = new Texture(options.normalMap, this.mainApp.appScene.object);
|
||||||
|
} else {
|
||||||
|
material.bumpTexture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用金属度贴图
|
||||||
|
if (options.metallicTexture !== undefined) {
|
||||||
|
if (options.metallicTexture) {
|
||||||
|
material.metallicTexture = new Texture(options.metallicTexture, this.mainApp.appScene.object);
|
||||||
|
} else {
|
||||||
|
material.metallicTexture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用粗糙度
|
||||||
|
if (options.roughness !== undefined) {
|
||||||
|
if (material.roughness !== options.roughness) {
|
||||||
|
material.roughness = options.roughness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用金属度
|
||||||
|
if (options.metallic !== undefined) {
|
||||||
|
if (material.metallic !== options.metallic) {
|
||||||
|
material.metallic = options.metallic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卷帘门开合:再次调用会反向动作 */
|
||||||
|
toggleRollerDoor(options?: RollerDoorOptions): void {
|
||||||
|
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直接设置卷帘门状态 */
|
||||||
|
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) {
|
||||||
|
console.warn('Scene not found for roller door');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cacheRollerDoorMeshes(options?.meshNames);
|
||||||
|
|
||||||
|
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
||||||
|
console.warn('Roller door group or meshes not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const upY = options?.upY ?? (this.rollerDoorInitialY.get(this.rollerDoorMeshes[0].name) || 0) + 3;
|
||||||
|
const downY = options?.downY ?? (this.rollerDoorInitialY.get(this.rollerDoorMeshes[0].name) || 0);
|
||||||
|
const speed = options?.speed ?? 1;
|
||||||
|
|
||||||
|
const targetY = open ? upY : downY;
|
||||||
|
|
||||||
|
if (this.rollerDoorObserver) {
|
||||||
|
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||||
|
this.rollerDoorObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
||||||
|
if (!this.rollerDoorGroup) return;
|
||||||
|
|
||||||
|
const delta = scene.getEngine().getDeltaTime() / 1000;
|
||||||
|
const step = speed * delta;
|
||||||
|
const currentY = this.rollerDoorGroup.position.y;
|
||||||
|
|
||||||
|
if (Math.abs(currentY - targetY) < step) {
|
||||||
|
this.rollerDoorGroup.position.y = targetY;
|
||||||
|
if (this.rollerDoorObserver) {
|
||||||
|
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||||
|
this.rollerDoorObserver = null;
|
||||||
|
}
|
||||||
|
this.rollerDoorIsOpen = open;
|
||||||
|
} else {
|
||||||
|
this.rollerDoorGroup.position.y += (targetY > currentY ? step : -step);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询卷帘门当前是否已开启 */
|
||||||
|
isRollerDoorOpen(): boolean {
|
||||||
|
return this.rollerDoorIsOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存卷帘门网格
|
||||||
|
*/
|
||||||
|
private cacheRollerDoorMeshes(meshNames?: string[]): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
const targetNames = meshNames || this.rollerDoorNames;
|
||||||
|
|
||||||
|
this.rollerDoorMeshes = scene.meshes.filter(mesh =>
|
||||||
|
targetNames.includes(mesh.name)
|
||||||
|
) as AbstractMesh[];
|
||||||
|
|
||||||
|
if (this.rollerDoorMeshes.length === 0) return;
|
||||||
|
|
||||||
|
// 记录初始Y坐标
|
||||||
|
this.rollerDoorMeshes.forEach(mesh => {
|
||||||
|
if (!this.rollerDoorInitialY.has(mesh.name)) {
|
||||||
|
this.rollerDoorInitialY.set(mesh.name, mesh.position.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建父节点统一控制
|
||||||
|
if (!this.rollerDoorGroup) {
|
||||||
|
this.rollerDoorGroup = this.rollerDoorMeshes[0];
|
||||||
|
for (let i = 1; i < this.rollerDoorMeshes.length; i++) {
|
||||||
|
this.rollerDoorMeshes[i].setParent(this.rollerDoorGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Y轴剖切:保留指定高度以上或以下的部分
|
||||||
|
*/
|
||||||
|
setYAxisClip(
|
||||||
|
height: number,
|
||||||
|
keepAbove: boolean = true,
|
||||||
|
onlyMeshNames?: string[],
|
||||||
|
excludeMeshNames?: string[]
|
||||||
|
): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
this.yClipPlane = new Plane(0, keepAbove ? -1 : 1, 0, keepAbove ? height : -height);
|
||||||
|
this.yClipTargets = onlyMeshNames || null;
|
||||||
|
|
||||||
|
scene.meshes.forEach(mesh => {
|
||||||
|
if (excludeMeshNames && excludeMeshNames.includes(mesh.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.yClipTargets && !this.yClipTargets.includes(mesh.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mesh.material) {
|
||||||
|
const materials = Array.isArray(mesh.material)
|
||||||
|
? (mesh.material as any[])
|
||||||
|
: [mesh.material];
|
||||||
|
|
||||||
|
materials.forEach(mat => {
|
||||||
|
if (!mat.clipPlane) {
|
||||||
|
mat.clipPlane = this.yClipPlane;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭Y轴剖切
|
||||||
|
*/
|
||||||
|
clearYAxisClip(): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene || !this.yClipPlane) return;
|
||||||
|
|
||||||
|
scene.meshes.forEach(mesh => {
|
||||||
|
if (mesh.material) {
|
||||||
|
const materials = Array.isArray(mesh.material)
|
||||||
|
? (mesh.material as any[])
|
||||||
|
: [mesh.material];
|
||||||
|
|
||||||
|
materials.forEach(mat => {
|
||||||
|
if (mat.clipPlane === this.yClipPlane) {
|
||||||
|
mat.clipPlane = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.yClipPlane = null;
|
||||||
|
this.yClipTargets = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
846
src/babylonjs/GameManager.old.ts
Normal file
@ -0,0 +1,846 @@
|
|||||||
|
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3, TransformNode } from "@babylonjs/core";
|
||||||
|
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||||
|
import { Nullable } from "@babylonjs/core/types";
|
||||||
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
|
import { Dictionary } from '../utils/Dictionary';
|
||||||
|
import { AppConfig } from './AppConfig';
|
||||||
|
|
||||||
|
type RollerDoorOptions = {
|
||||||
|
/** 目标升起高度,缺省为初始 y + 3 */
|
||||||
|
upY?: number;
|
||||||
|
/** 落下终点,缺省为初始 y */
|
||||||
|
downY?: number;
|
||||||
|
/** 运动速度(单位/秒),缺省 1 */
|
||||||
|
speed?: number;
|
||||||
|
/** 自定义门体网格名列表,不传使用默认两个卷帘门 */
|
||||||
|
meshNames?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏管理器类 - 负责管理游戏逻辑、材质和纹理
|
||||||
|
*/
|
||||||
|
export class GameManager extends Monobehiver {
|
||||||
|
private materialDic: Dictionary<PBRMaterial>;
|
||||||
|
private meshDic: Dictionary<any>;
|
||||||
|
private oldTextureDic: Dictionary<any>;
|
||||||
|
private rollerDoorMeshes: AbstractMesh[];
|
||||||
|
private rollerDoorGroup: AbstractMesh | null;
|
||||||
|
private rollerDoorInitialY: Map<string, number>;
|
||||||
|
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
||||||
|
private rollerDoorIsOpen: boolean;
|
||||||
|
private rollerDoorNames: string[];
|
||||||
|
private yClipPlane: Plane | null;
|
||||||
|
private yClipTargets: string[] | null;
|
||||||
|
private clipPlaneVisualization: Mesh | null;
|
||||||
|
|
||||||
|
// 记录加载失败的贴图
|
||||||
|
private failedTextures: Array<{
|
||||||
|
path: string;
|
||||||
|
materialName?: string;
|
||||||
|
textureType?: string;
|
||||||
|
error?: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
constructor(mainApp: any) {
|
||||||
|
super(mainApp);
|
||||||
|
this.materialDic = new Dictionary<PBRMaterial>();
|
||||||
|
this.meshDic = new Dictionary<any>();
|
||||||
|
this.oldTextureDic = new Dictionary<any>();
|
||||||
|
this.rollerDoorMeshes = [];
|
||||||
|
this.rollerDoorGroup = null;
|
||||||
|
this.rollerDoorInitialY = new Map();
|
||||||
|
this.rollerDoorObserver = null;
|
||||||
|
this.rollerDoorIsOpen = false;
|
||||||
|
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
||||||
|
this.yClipPlane = null;
|
||||||
|
this.yClipTargets = null;
|
||||||
|
this.clipPlaneVisualization = null;
|
||||||
|
this.failedTextures = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 调试:返回当前场景中所有网格名称 */
|
||||||
|
listMeshNames(): string[] {
|
||||||
|
return this.meshDic.Keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化游戏管理器 */
|
||||||
|
async Awake() {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) {
|
||||||
|
console.warn('Scene not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化材质和网格字典
|
||||||
|
this.updateDictionaries();
|
||||||
|
|
||||||
|
// this.cacheRollerDoorMeshes();
|
||||||
|
|
||||||
|
// this.setRollerDoorScale("Box006.001", new Vector3(0.12, 0.02, 0.118));
|
||||||
|
// this.setRollerDoorScale("Box005.001", new Vector3(0.13, 0.02, 0.12));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新材质和网格字典(从场景中同步)
|
||||||
|
*/
|
||||||
|
updateDictionaries(): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
this.materialDic.Clear();
|
||||||
|
this.meshDic.Clear();
|
||||||
|
|
||||||
|
// 更新材质字典
|
||||||
|
for (const mat of scene.materials) {
|
||||||
|
if (!(mat as any)._isDisposed && !this.materialDic.Has(mat.name)) {
|
||||||
|
this.materialDic.Set(mat.name, mat as PBRMaterial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新网格字典
|
||||||
|
for (const mesh of scene.meshes) {
|
||||||
|
if (mesh instanceof Mesh && !mesh.isDisposed() && !this.meshDic.Has(mesh.name)) {
|
||||||
|
this.meshDic.Set(mesh.name, mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mat = mesh.material;
|
||||||
|
if (mat instanceof PBRMaterial && !(mat as any)._isDisposed) {
|
||||||
|
this.materialDic.Set(mat.name, mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化设置材质 */
|
||||||
|
async initSetMaterial(oldObject: any) {
|
||||||
|
if (!oldObject?.Component?.length) return;
|
||||||
|
|
||||||
|
|
||||||
|
const { degreeId, Component } = oldObject;
|
||||||
|
let degreeTextureDic = this.oldTextureDic.Get(degreeId) || {};
|
||||||
|
const texturePromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
// 处理每个组件
|
||||||
|
for (const component of Component) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
albedoTexture,
|
||||||
|
bumpTexture,
|
||||||
|
alphaTexture,
|
||||||
|
aoTexture,
|
||||||
|
} = component;
|
||||||
|
|
||||||
|
if (!name) continue;
|
||||||
|
|
||||||
|
// 获取材质
|
||||||
|
const mat = this.materialDic.Get(name);
|
||||||
|
if (!mat) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或初始化纹理字典
|
||||||
|
const textureDic = degreeTextureDic[name] || {
|
||||||
|
albedo: null,
|
||||||
|
bump: null,
|
||||||
|
alpha: null,
|
||||||
|
ao: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义纹理任务
|
||||||
|
const textureTasks = [
|
||||||
|
{
|
||||||
|
key: "albedo",
|
||||||
|
path: albedoTexture,
|
||||||
|
property: "albedoTexture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "bump",
|
||||||
|
path: bumpTexture,
|
||||||
|
property: "bumpTexture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "alpha",
|
||||||
|
path: alphaTexture,
|
||||||
|
property: "opacityTexture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ao",
|
||||||
|
path: aoTexture,
|
||||||
|
property: "ambientTexture"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 处理每个纹理任务
|
||||||
|
for (const task of textureTasks) {
|
||||||
|
const { key, path, property } = task;
|
||||||
|
if (!path) continue;
|
||||||
|
|
||||||
|
const fullPath = this.getPublicUrl() + path;
|
||||||
|
let texture = textureDic[key];
|
||||||
|
|
||||||
|
if (!texture) {
|
||||||
|
try {
|
||||||
|
texture = this.createTextureWithFallback(fullPath);
|
||||||
|
if (!texture) {
|
||||||
|
// 记录失败的贴图信息
|
||||||
|
this.failedTextures.push({
|
||||||
|
path: fullPath,
|
||||||
|
materialName: name,
|
||||||
|
textureType: key,
|
||||||
|
error: '贴图创建失败',
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置非ktx2格式的vScale
|
||||||
|
if (!fullPath.toLowerCase().endsWith('.ktx2')) {
|
||||||
|
texture.vScale = -1;
|
||||||
|
}
|
||||||
|
textureDic[key] = texture;
|
||||||
|
} catch (error: any) {
|
||||||
|
// 记录失败的贴图信息
|
||||||
|
this.failedTextures.push({
|
||||||
|
path: fullPath,
|
||||||
|
materialName: name,
|
||||||
|
textureType: key,
|
||||||
|
error: error.message || error.toString(),
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将纹理赋值任务加入队列
|
||||||
|
texturePromises.push(
|
||||||
|
this.handleTextureAssignment(mat, textureDic, key, (texture) => {
|
||||||
|
(mat as any)[property] = texture;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新纹理字典
|
||||||
|
degreeTextureDic[name] = textureDic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有纹理任务完成
|
||||||
|
try {
|
||||||
|
await Promise.all(texturePromises);
|
||||||
|
|
||||||
|
// 在所有贴图加载完成后设置材质属性
|
||||||
|
for (const component of Component) {
|
||||||
|
const { name, transparencyMode, bumpTextureLevel } = component;
|
||||||
|
if (!name) continue;
|
||||||
|
|
||||||
|
const mat = this.materialDic.Get(name);
|
||||||
|
if (!mat) continue;
|
||||||
|
|
||||||
|
mat.transparencyMode = transparencyMode;
|
||||||
|
|
||||||
|
if (mat.bumpTexture) {
|
||||||
|
mat.bumpTexture.level = bumpTextureLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用新的PBR材质属性
|
||||||
|
this.applyPBRProperties(mat, component);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading textures:', error);
|
||||||
|
} finally {
|
||||||
|
if (this.mainApp.appDom?.load3D) {
|
||||||
|
this.mainApp.appDom.load3D.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新后的纹理字典
|
||||||
|
this.oldTextureDic.Set(degreeId, degreeTextureDic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用PBR材质属性
|
||||||
|
* @param mat - PBR材质对象
|
||||||
|
* @param component - 配置组件对象
|
||||||
|
*/
|
||||||
|
private applyPBRProperties(mat: PBRMaterial, component: any) {
|
||||||
|
// 定义PBR属性映射任务
|
||||||
|
const pbrTasks = [
|
||||||
|
{
|
||||||
|
key: "fresnel",
|
||||||
|
value: component.fresnel,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.indexOfRefraction = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "clearcoat",
|
||||||
|
value: component.clearcoat,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.clearCoat.isEnabled = true;
|
||||||
|
mat.clearCoat.intensity = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "clearcoatRoughness",
|
||||||
|
value: component.clearcoatRoughness,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.clearCoat.roughness = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "roughness",
|
||||||
|
value: component.roughness,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.roughness = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metallic",
|
||||||
|
value: component.metallic,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.metallic = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "alpha",
|
||||||
|
value: component.alpha,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.alpha = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "environmentIntensity",
|
||||||
|
value: component.environmentIntensity,
|
||||||
|
apply: (value: number) => {
|
||||||
|
mat.environmentIntensity = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "baseColor",
|
||||||
|
value: component.baseColor,
|
||||||
|
apply: (value: any) => {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const { r, g, b } = value;
|
||||||
|
if (r !== null && r !== undefined &&
|
||||||
|
g !== null && g !== undefined &&
|
||||||
|
b !== null && b !== undefined) {
|
||||||
|
mat.albedoColor.set(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 处理每个PBR属性任务
|
||||||
|
for (const task of pbrTasks) {
|
||||||
|
if (task.value !== null && task.value !== undefined) {
|
||||||
|
try {
|
||||||
|
task.apply(task.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error applying PBR property:', task.key, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通用的批量卸载贴图资源的方法 */
|
||||||
|
private clearTextures(textureDic: Dictionary<any>): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
textureDic.Values().forEach((textures) => {
|
||||||
|
for (const key in textures) {
|
||||||
|
const texture = textures[key];
|
||||||
|
if (texture && texture instanceof Texture) {
|
||||||
|
texture.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
textureDic.Clear();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理纹理赋值 */
|
||||||
|
private async handleTextureAssignment(mat: any, oldtextureDic: any, textureKey: string, assignCallback: (texture: Texture) => void) {
|
||||||
|
const texture = oldtextureDic[textureKey];
|
||||||
|
if (texture) {
|
||||||
|
await this.checkTextureLoadedWithPromise(texture);
|
||||||
|
assignCallback(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检查纹理是否加载完成 */
|
||||||
|
private checkTextureLoadedWithPromise(texture: Texture): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (texture.isReady()) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
texture.onLoadObservable.addOnce(() => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置相机位置 */
|
||||||
|
reSet() {
|
||||||
|
if (this.mainApp.appCamera?.object?.position) {
|
||||||
|
this.mainApp.appCamera.object.position.set(160, 50, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卷帘门开合:再次调用会反向动作 */
|
||||||
|
toggleRollerDoor(options?: RollerDoorOptions): void {
|
||||||
|
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直接设置卷帘门状态 */
|
||||||
|
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) {
|
||||||
|
console.warn('Scene not found for roller door');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cacheRollerDoorMeshes(options?.meshNames);
|
||||||
|
|
||||||
|
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
||||||
|
console.warn('Roller door group or meshes not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const speed = Math.max(options?.speed ?? 1, 0.01);
|
||||||
|
|
||||||
|
// 计算目标高度
|
||||||
|
let targetY: number;
|
||||||
|
if (open) {
|
||||||
|
// 上升时:如果指定了绝对高度就用绝对高度,否则用相对高度
|
||||||
|
if (options?.upY !== undefined) {
|
||||||
|
targetY = options.upY;
|
||||||
|
} else {
|
||||||
|
// 找到所有门中最高的初始位置,让所有门都升到这个高度+3
|
||||||
|
const maxBaseY = Math.max(...this.rollerDoorMeshes.map(m =>
|
||||||
|
this.rollerDoorInitialY.get(m.name) ?? m.position.y
|
||||||
|
));
|
||||||
|
targetY = maxBaseY + 3;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 下降时:回到初始位置
|
||||||
|
targetY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经在目标位置
|
||||||
|
if (Math.abs(this.rollerDoorGroup.position.y - targetY) < 0.001) {
|
||||||
|
this.rollerDoorIsOpen = open;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rollerDoorIsOpen = open;
|
||||||
|
this.stopRollerDoorAnimation();
|
||||||
|
|
||||||
|
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
||||||
|
const dt = scene.getEngine().getDeltaTime() / 1000;
|
||||||
|
const current = this.rollerDoorGroup!.position.y;
|
||||||
|
const direction = targetY >= current ? 1 : -1;
|
||||||
|
|
||||||
|
// 使用固定速度变量
|
||||||
|
const step = speed * dt;
|
||||||
|
let next = current + direction * step;
|
||||||
|
|
||||||
|
if ((direction > 0 && next >= targetY) || (direction < 0 && next <= targetY)) {
|
||||||
|
next = targetY;
|
||||||
|
this.stopRollerDoorAnimation();
|
||||||
|
this.rollerDoorIsOpen = open;
|
||||||
|
console.log('Roller door animation finished');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动透明盒子
|
||||||
|
this.rollerDoorGroup!.position.y = next;
|
||||||
|
|
||||||
|
// 打印每个卷帘门的当前位置
|
||||||
|
// console.log('Roller door positions:');
|
||||||
|
// for (const mesh of this.rollerDoorMeshes) {
|
||||||
|
// console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前卷帘门是否开启 */
|
||||||
|
isRollerDoorOpen(): boolean {
|
||||||
|
return this.rollerDoorIsOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置卷帘门的缩放
|
||||||
|
* @param meshName - 卷帘门网格名称
|
||||||
|
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||||
|
*/
|
||||||
|
setRollerDoorScale(meshName: string, scale: number | Vector3): void {
|
||||||
|
const mesh = this.meshDic.Get(meshName);
|
||||||
|
if (mesh) {
|
||||||
|
if (typeof scale === 'number') {
|
||||||
|
mesh.scaling.set(scale, scale, scale);
|
||||||
|
} else {
|
||||||
|
mesh.scaling.copyFrom(scale);
|
||||||
|
}
|
||||||
|
console.log(`Set scale for ${meshName}:`, mesh.scaling.asArray());
|
||||||
|
} else {
|
||||||
|
console.warn(`Roller door mesh not found: ${meshName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置所有卷帘门的缩放
|
||||||
|
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
||||||
|
*/
|
||||||
|
setAllRollerDoorsScale(scale: number | Vector3): void {
|
||||||
|
this.rollerDoorMeshes.forEach(mesh => {
|
||||||
|
if (typeof scale === 'number') {
|
||||||
|
mesh.scaling.set(scale, scale, scale);
|
||||||
|
} else {
|
||||||
|
mesh.scaling.copyFrom(scale);
|
||||||
|
}
|
||||||
|
console.log(`Set scale for ${mesh.name}:`, mesh.scaling.asArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置基于 Y 轴的剖切平面,keepAbove=true 时保留平面以上部分
|
||||||
|
* onlyMeshNames 指定只作用于哪些网格,其他网格不受影响
|
||||||
|
*/
|
||||||
|
setYAxisClip(
|
||||||
|
height: number,
|
||||||
|
keepAbove = true,
|
||||||
|
onlyMeshNames?: string[],
|
||||||
|
excludeMeshNames?: string[]
|
||||||
|
): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) {
|
||||||
|
console.warn('Scene not found for clipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normal = new Vector3(0, keepAbove ? 1 : -1, 0);
|
||||||
|
this.yClipPlane = Plane.FromPositionAndNormal(new Vector3(0, height, 0), normal);
|
||||||
|
|
||||||
|
// 如果指定了特定网格,只对这些网格应用剖切
|
||||||
|
if (onlyMeshNames?.length) {
|
||||||
|
this.applyClipPlaneToMeshes(this.yClipPlane, onlyMeshNames);
|
||||||
|
} else {
|
||||||
|
// 否则使用场景级别的剖切,作用于所有网格
|
||||||
|
scene.clipPlane = this.yClipPlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[clipping] Scene clipPlane set:', { height, keepAbove, normal: normal.asArray(), targets: onlyMeshNames || 'all' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭 Y 轴剖切 */
|
||||||
|
clearYAxisClip(): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (scene) {
|
||||||
|
scene.clipPlane = null;
|
||||||
|
}
|
||||||
|
this.yClipPlane = null;
|
||||||
|
this.yClipTargets = null;
|
||||||
|
|
||||||
|
// 清除所有网格材质上的 clipPlane
|
||||||
|
this.meshDic.Values().forEach((mesh) => {
|
||||||
|
const mat = mesh.material as any;
|
||||||
|
if (mat && 'clipPlane' in mat) {
|
||||||
|
mat.clipPlane = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private cacheRollerDoorMeshes(customNames?: string[]): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
const names = customNames?.length ? customNames : this.rollerDoorNames;
|
||||||
|
this.rollerDoorMeshes = [];
|
||||||
|
|
||||||
|
// 创建或获取 group 作为父级
|
||||||
|
if (!this.rollerDoorGroup) {
|
||||||
|
// 创建一个 AbstractMesh 作为组
|
||||||
|
// 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化
|
||||||
|
this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any;
|
||||||
|
// 确保 group 的初始位置为 (0, 0, 0)
|
||||||
|
this.rollerDoorGroup.position.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const mesh = this.meshDic.Get(name);
|
||||||
|
if (mesh) {
|
||||||
|
this.rollerDoorMeshes.push(mesh);
|
||||||
|
|
||||||
|
// 保存网格的当前位置作为初始位置
|
||||||
|
if (!this.rollerDoorInitialY.has(name)) {
|
||||||
|
this.rollerDoorInitialY.set(name, mesh.position.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存网格的世界位置和缩放
|
||||||
|
const worldPosition = mesh.getAbsolutePosition();
|
||||||
|
const worldScaling = new Vector3(mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);
|
||||||
|
|
||||||
|
// 将网格添加到 group 中
|
||||||
|
mesh.parent = this.rollerDoorGroup;
|
||||||
|
|
||||||
|
// 调整网格的局部位置和缩放,保持世界位置和大小不变
|
||||||
|
mesh.setAbsolutePosition(worldPosition);
|
||||||
|
mesh.scaling.copyFrom(worldScaling);
|
||||||
|
} else {
|
||||||
|
console.warn(`Roller door mesh not found: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopRollerDoorAnimation(): void {
|
||||||
|
const scene = this.mainApp.appScene?.object;
|
||||||
|
if (scene && this.rollerDoorObserver) {
|
||||||
|
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
||||||
|
}
|
||||||
|
this.rollerDoorObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 clipPlane 只作用到指定网格的材质 */
|
||||||
|
private applyClipPlaneToMeshes(plane: Plane, targetNames: string[]): void {
|
||||||
|
const targetSet = new Set(targetNames);
|
||||||
|
let appliedCount = 0;
|
||||||
|
|
||||||
|
this.meshDic.Values().forEach((mesh) => {
|
||||||
|
const mat = mesh.material as any;
|
||||||
|
if (!mat) {
|
||||||
|
console.log('[clipping] Mesh has no material:', mesh.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetSet.has(mesh.name)) {
|
||||||
|
// 目标网格:应用剖切
|
||||||
|
mat.clipPlane = plane;
|
||||||
|
appliedCount++;
|
||||||
|
console.log('[clipping] Applied to mesh:', mesh.name, 'material:', mat.name);
|
||||||
|
} else {
|
||||||
|
// 非目标网格:清除剖切
|
||||||
|
mat.clipPlane = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[clipping] Total meshes processed:', this.meshDic.Keys().length, 'Applied to:', appliedCount);
|
||||||
|
if (appliedCount === 0) {
|
||||||
|
console.warn('[clipping] No meshes found with names:', targetNames);
|
||||||
|
console.log('[clipping] Available mesh names:', this.meshDic.Keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取公共URL */
|
||||||
|
private getPublicUrl(): string {
|
||||||
|
// 尝试从环境变量获取
|
||||||
|
if (import.meta && import.meta.env && import.meta.env.VITE_PUBLIC_URL) {
|
||||||
|
return import.meta.env.VITE_PUBLIC_URL;
|
||||||
|
}
|
||||||
|
// 默认返回空字符串
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理资源 */
|
||||||
|
dispose() {
|
||||||
|
this.stopRollerDoorAnimation();
|
||||||
|
this.clearYAxisClip();
|
||||||
|
this.rollerDoorMeshes = [];
|
||||||
|
this.rollerDoorInitialY.clear();
|
||||||
|
this.rollerDoorIsOpen = false;
|
||||||
|
|
||||||
|
// 清理 rollerDoorGroup
|
||||||
|
if (this.rollerDoorGroup && this.rollerDoorGroup.dispose) {
|
||||||
|
this.rollerDoorGroup.dispose();
|
||||||
|
this.rollerDoorGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有材质资源
|
||||||
|
this.materialDic.Values().forEach((material) => {
|
||||||
|
if (material && material.dispose) {
|
||||||
|
material.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.materialDic.Clear();
|
||||||
|
|
||||||
|
// 清理所有贴图资源
|
||||||
|
this.clearTextures(this.oldTextureDic);
|
||||||
|
|
||||||
|
// 清理所有网格
|
||||||
|
this.meshDic.Values().forEach((mesh) => {
|
||||||
|
if (mesh && mesh.dispose) {
|
||||||
|
mesh.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.meshDic.Clear();
|
||||||
|
|
||||||
|
// 清空失败贴图记录
|
||||||
|
this.failedTextures = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新 */
|
||||||
|
update() { }
|
||||||
|
|
||||||
|
/** 尝试创建贴图的方法,支持多种格式回退 */
|
||||||
|
private createTextureWithFallback(texturePath: string): Texture | null {
|
||||||
|
const failureReasons: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const texture = new Texture(texturePath);
|
||||||
|
|
||||||
|
if (texture) {
|
||||||
|
return texture;
|
||||||
|
} else {
|
||||||
|
failureReasons.push(`原始路径创建失败: ${texturePath}`);
|
||||||
|
throw new Error('Texture creation returned null');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
|
// 特别处理KTX错误
|
||||||
|
if (errorMessage.includes('KTX identifier') || errorMessage.includes('missing KTX') ||
|
||||||
|
(texturePath.toLowerCase().endsWith('.ktx2') && errorMessage)) {
|
||||||
|
this.failedTextures.push({
|
||||||
|
path: texturePath,
|
||||||
|
textureType: 'KTX2',
|
||||||
|
error: `KTX错误: ${errorMessage}`,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
failureReasons.push(`原始路径加载异常: ${texturePath} - ${errorMessage}`);
|
||||||
|
|
||||||
|
// 如果是ktx2文件加载失败,尝试查找对应的jpg/png文件
|
||||||
|
if (texturePath.toLowerCase().endsWith('.ktx2')) {
|
||||||
|
// 尝试jpg格式
|
||||||
|
const jpgPath = texturePath.replace(/\.ktx2$/i, '.jpg');
|
||||||
|
try {
|
||||||
|
const jpgTexture = new Texture(jpgPath);
|
||||||
|
if (jpgTexture) {
|
||||||
|
return jpgTexture;
|
||||||
|
}
|
||||||
|
} catch (jpgError: any) {
|
||||||
|
failureReasons.push(`JPG回退失败: ${jpgPath} - ${jpgError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试png格式
|
||||||
|
const pngPath = texturePath.replace(/\.ktx2$/i, '.png');
|
||||||
|
try {
|
||||||
|
const pngTexture = new Texture(pngPath);
|
||||||
|
if (pngTexture) {
|
||||||
|
return pngTexture;
|
||||||
|
}
|
||||||
|
} catch (pngError: any) {
|
||||||
|
failureReasons.push(`PNG回退失败: ${pngPath} - ${pngError}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有格式都失败,记录详细失败信息
|
||||||
|
this.failedTextures.push({
|
||||||
|
path: texturePath,
|
||||||
|
textureType: '回退机制',
|
||||||
|
error: failureReasons.join('; '),
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用材质属性
|
||||||
|
* @param options 材质配置选项
|
||||||
|
*/
|
||||||
|
applyMaterial(options: {
|
||||||
|
target: string;
|
||||||
|
modelId?: string;
|
||||||
|
albedoColor?: string;
|
||||||
|
albedoTexture?: string;
|
||||||
|
normalMap?: string;
|
||||||
|
metallicTexture?: string;
|
||||||
|
roughness?: number;
|
||||||
|
metallic?: number;
|
||||||
|
}): void {
|
||||||
|
this.updateDictionaries();
|
||||||
|
|
||||||
|
// 查找目标材质(支持精确匹配和前缀匹配)
|
||||||
|
const targetMaterials: PBRMaterial[] = [];
|
||||||
|
|
||||||
|
// 如果提供了 modelId,只查找该模型的材质
|
||||||
|
if (options.modelId) {
|
||||||
|
// 获取该模型的所有 meshes
|
||||||
|
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
||||||
|
|
||||||
|
if (!modelMeshes || modelMeshes.length === 0) {
|
||||||
|
console.warn(`Model not found: ${options.modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历该模型的所有 mesh,查找匹配的材质
|
||||||
|
modelMeshes.forEach((mesh: AbstractMesh) => {
|
||||||
|
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
||||||
|
const material = mesh.material as PBRMaterial;
|
||||||
|
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||||
|
// 避免重复添加
|
||||||
|
if (!targetMaterials.includes(material)) {
|
||||||
|
targetMaterials.push(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 没有提供 modelId,全局查找(保持向后兼容)
|
||||||
|
this.materialDic.Values().forEach(material => {
|
||||||
|
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||||
|
console.log(material.name);
|
||||||
|
targetMaterials.push(material);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetMaterials.length === 0) {
|
||||||
|
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(options);
|
||||||
|
// 应用材质属性到目标材质
|
||||||
|
targetMaterials.forEach(material => {
|
||||||
|
// 应用颜色
|
||||||
|
if (options.albedoColor) {
|
||||||
|
const color = Color3.FromHexString(options.albedoColor);
|
||||||
|
material.albedoColor.copyFrom(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//应用反照率纹理(颜色贴图)
|
||||||
|
if (options.albedoTexture !== undefined) {
|
||||||
|
if (options.albedoTexture) {
|
||||||
|
material.albedoTexture = new Texture(options.albedoTexture);
|
||||||
|
} else {
|
||||||
|
// 传入空字符串或 null 时清空贴图
|
||||||
|
material.albedoTexture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用粗糙度
|
||||||
|
if (options.roughness !== undefined) {
|
||||||
|
if (material.roughness !== options.roughness) {
|
||||||
|
material.roughness = options.roughness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用金属度
|
||||||
|
if (options.metallic !== undefined) {
|
||||||
|
if (material.metallic !== options.metallic) {
|
||||||
|
material.metallic = options.metallic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 强制刷新材质
|
||||||
|
material.markDirty();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,62 +1,18 @@
|
|||||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3, TransformNode } from "@babylonjs/core";
|
import { Mesh, PBRMaterial, Texture, AbstractMesh, Color3 } from "@babylonjs/core";
|
||||||
import { Observer } from "@babylonjs/core/Misc/observable";
|
|
||||||
import { Nullable } from "@babylonjs/core/types";
|
|
||||||
import { Monobehiver } from '../base/Monobehiver';
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
import { Dictionary } from '../utils/Dictionary';
|
import { Dictionary } from '../utils/Dictionary';
|
||||||
import { AppConfig } from './AppConfig';
|
|
||||||
|
|
||||||
type RollerDoorOptions = {
|
|
||||||
/** 目标升起高度,缺省为初始 y + 3 */
|
|
||||||
upY?: number;
|
|
||||||
/** 落下终点,缺省为初始 y */
|
|
||||||
downY?: number;
|
|
||||||
/** 运动速度(单位/秒),缺省 1 */
|
|
||||||
speed?: number;
|
|
||||||
/** 自定义门体网格名列表,不传使用默认两个卷帘门 */
|
|
||||||
meshNames?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏管理器类 - 负责管理游戏逻辑、材质和纹理
|
* 游戏管理器类 - 负责材质管理
|
||||||
*/
|
*/
|
||||||
export class GameManager extends Monobehiver {
|
export class GameManager extends Monobehiver {
|
||||||
private materialDic: Dictionary<PBRMaterial>;
|
private materialDic: Dictionary<PBRMaterial>;
|
||||||
private meshDic: Dictionary<any>;
|
private meshDic: Dictionary<any>;
|
||||||
private oldTextureDic: Dictionary<any>;
|
|
||||||
private rollerDoorMeshes: AbstractMesh[];
|
|
||||||
private rollerDoorGroup: AbstractMesh | null;
|
|
||||||
private rollerDoorInitialY: Map<string, number>;
|
|
||||||
private rollerDoorObserver: Nullable<Observer<Scene>>;
|
|
||||||
private rollerDoorIsOpen: boolean;
|
|
||||||
private rollerDoorNames: string[];
|
|
||||||
private yClipPlane: Plane | null;
|
|
||||||
private yClipTargets: string[] | null;
|
|
||||||
private clipPlaneVisualization: Mesh | null;
|
|
||||||
|
|
||||||
// 记录加载失败的贴图
|
|
||||||
private failedTextures: Array<{
|
|
||||||
path: string;
|
|
||||||
materialName?: string;
|
|
||||||
textureType?: string;
|
|
||||||
error?: string;
|
|
||||||
timestamp: Date;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
constructor(mainApp: any) {
|
constructor(mainApp: any) {
|
||||||
super(mainApp);
|
super(mainApp);
|
||||||
this.materialDic = new Dictionary<PBRMaterial>();
|
this.materialDic = new Dictionary<PBRMaterial>();
|
||||||
this.meshDic = new Dictionary<any>();
|
this.meshDic = new Dictionary<any>();
|
||||||
this.oldTextureDic = new Dictionary<any>();
|
|
||||||
this.rollerDoorMeshes = [];
|
|
||||||
this.rollerDoorGroup = null;
|
|
||||||
this.rollerDoorInitialY = new Map();
|
|
||||||
this.rollerDoorObserver = null;
|
|
||||||
this.rollerDoorIsOpen = false;
|
|
||||||
this.rollerDoorNames = ["Box006.001", "Box005.001"];
|
|
||||||
this.yClipPlane = null;
|
|
||||||
this.yClipTargets = null;
|
|
||||||
this.clipPlaneVisualization = null;
|
|
||||||
this.failedTextures = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 调试:返回当前场景中所有网格名称 */
|
/** 调试:返回当前场景中所有网格名称 */
|
||||||
@ -72,13 +28,7 @@ export class GameManager extends Monobehiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化材质和网格字典
|
|
||||||
this.updateDictionaries();
|
this.updateDictionaries();
|
||||||
|
|
||||||
// this.cacheRollerDoorMeshes();
|
|
||||||
|
|
||||||
// this.setRollerDoorScale("Box006.001", new Vector3(0.12, 0.02, 0.118));
|
|
||||||
// this.setRollerDoorScale("Box005.001", new Vector3(0.13, 0.02, 0.12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,640 +61,6 @@ export class GameManager extends Monobehiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化设置材质 */
|
|
||||||
async initSetMaterial(oldObject: any) {
|
|
||||||
if (!oldObject?.Component?.length) return;
|
|
||||||
|
|
||||||
|
|
||||||
const { degreeId, Component } = oldObject;
|
|
||||||
let degreeTextureDic = this.oldTextureDic.Get(degreeId) || {};
|
|
||||||
const texturePromises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
// 处理每个组件
|
|
||||||
for (const component of Component) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
albedoTexture,
|
|
||||||
bumpTexture,
|
|
||||||
alphaTexture,
|
|
||||||
aoTexture,
|
|
||||||
} = component;
|
|
||||||
|
|
||||||
if (!name) continue;
|
|
||||||
|
|
||||||
// 获取材质
|
|
||||||
const mat = this.materialDic.Get(name);
|
|
||||||
if (!mat) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取或初始化纹理字典
|
|
||||||
const textureDic = degreeTextureDic[name] || {
|
|
||||||
albedo: null,
|
|
||||||
bump: null,
|
|
||||||
alpha: null,
|
|
||||||
ao: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 定义纹理任务
|
|
||||||
const textureTasks = [
|
|
||||||
{
|
|
||||||
key: "albedo",
|
|
||||||
path: albedoTexture,
|
|
||||||
property: "albedoTexture"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "bump",
|
|
||||||
path: bumpTexture,
|
|
||||||
property: "bumpTexture"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "alpha",
|
|
||||||
path: alphaTexture,
|
|
||||||
property: "opacityTexture"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "ao",
|
|
||||||
path: aoTexture,
|
|
||||||
property: "ambientTexture"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 处理每个纹理任务
|
|
||||||
for (const task of textureTasks) {
|
|
||||||
const { key, path, property } = task;
|
|
||||||
if (!path) continue;
|
|
||||||
|
|
||||||
const fullPath = this.getPublicUrl() + path;
|
|
||||||
let texture = textureDic[key];
|
|
||||||
|
|
||||||
if (!texture) {
|
|
||||||
try {
|
|
||||||
texture = this.createTextureWithFallback(fullPath);
|
|
||||||
if (!texture) {
|
|
||||||
// 记录失败的贴图信息
|
|
||||||
this.failedTextures.push({
|
|
||||||
path: fullPath,
|
|
||||||
materialName: name,
|
|
||||||
textureType: key,
|
|
||||||
error: '贴图创建失败',
|
|
||||||
timestamp: new Date()
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置非ktx2格式的vScale
|
|
||||||
if (!fullPath.toLowerCase().endsWith('.ktx2')) {
|
|
||||||
texture.vScale = -1;
|
|
||||||
}
|
|
||||||
textureDic[key] = texture;
|
|
||||||
} catch (error: any) {
|
|
||||||
// 记录失败的贴图信息
|
|
||||||
this.failedTextures.push({
|
|
||||||
path: fullPath,
|
|
||||||
materialName: name,
|
|
||||||
textureType: key,
|
|
||||||
error: error.message || error.toString(),
|
|
||||||
timestamp: new Date()
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将纹理赋值任务加入队列
|
|
||||||
texturePromises.push(
|
|
||||||
this.handleTextureAssignment(mat, textureDic, key, (texture) => {
|
|
||||||
(mat as any)[property] = texture;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新纹理字典
|
|
||||||
degreeTextureDic[name] = textureDic;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待所有纹理任务完成
|
|
||||||
try {
|
|
||||||
await Promise.all(texturePromises);
|
|
||||||
|
|
||||||
// 在所有贴图加载完成后设置材质属性
|
|
||||||
for (const component of Component) {
|
|
||||||
const { name, transparencyMode, bumpTextureLevel } = component;
|
|
||||||
if (!name) continue;
|
|
||||||
|
|
||||||
const mat = this.materialDic.Get(name);
|
|
||||||
if (!mat) continue;
|
|
||||||
|
|
||||||
mat.transparencyMode = transparencyMode;
|
|
||||||
|
|
||||||
if (mat.bumpTexture) {
|
|
||||||
mat.bumpTexture.level = bumpTextureLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用新的PBR材质属性
|
|
||||||
this.applyPBRProperties(mat, component);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading textures:', error);
|
|
||||||
} finally {
|
|
||||||
if (this.mainApp.appDom?.load3D) {
|
|
||||||
this.mainApp.appDom.load3D.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存更新后的纹理字典
|
|
||||||
this.oldTextureDic.Set(degreeId, degreeTextureDic);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用PBR材质属性
|
|
||||||
* @param mat - PBR材质对象
|
|
||||||
* @param component - 配置组件对象
|
|
||||||
*/
|
|
||||||
private applyPBRProperties(mat: PBRMaterial, component: any) {
|
|
||||||
// 定义PBR属性映射任务
|
|
||||||
const pbrTasks = [
|
|
||||||
{
|
|
||||||
key: "fresnel",
|
|
||||||
value: component.fresnel,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.indexOfRefraction = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "clearcoat",
|
|
||||||
value: component.clearcoat,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.clearCoat.isEnabled = true;
|
|
||||||
mat.clearCoat.intensity = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "clearcoatRoughness",
|
|
||||||
value: component.clearcoatRoughness,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.clearCoat.roughness = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "roughness",
|
|
||||||
value: component.roughness,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.roughness = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "metallic",
|
|
||||||
value: component.metallic,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.metallic = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "alpha",
|
|
||||||
value: component.alpha,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.alpha = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "environmentIntensity",
|
|
||||||
value: component.environmentIntensity,
|
|
||||||
apply: (value: number) => {
|
|
||||||
mat.environmentIntensity = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "baseColor",
|
|
||||||
value: component.baseColor,
|
|
||||||
apply: (value: any) => {
|
|
||||||
if (value && typeof value === 'object') {
|
|
||||||
const { r, g, b } = value;
|
|
||||||
if (r !== null && r !== undefined &&
|
|
||||||
g !== null && g !== undefined &&
|
|
||||||
b !== null && b !== undefined) {
|
|
||||||
mat.albedoColor.set(r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 处理每个PBR属性任务
|
|
||||||
for (const task of pbrTasks) {
|
|
||||||
if (task.value !== null && task.value !== undefined) {
|
|
||||||
try {
|
|
||||||
task.apply(task.value);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Error applying PBR property:', task.key, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 通用的批量卸载贴图资源的方法 */
|
|
||||||
private clearTextures(textureDic: Dictionary<any>): Promise<void> {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
textureDic.Values().forEach((textures) => {
|
|
||||||
for (const key in textures) {
|
|
||||||
const texture = textures[key];
|
|
||||||
if (texture && texture instanceof Texture) {
|
|
||||||
texture.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
textureDic.Clear();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理纹理赋值 */
|
|
||||||
private async handleTextureAssignment(mat: any, oldtextureDic: any, textureKey: string, assignCallback: (texture: Texture) => void) {
|
|
||||||
const texture = oldtextureDic[textureKey];
|
|
||||||
if (texture) {
|
|
||||||
await this.checkTextureLoadedWithPromise(texture);
|
|
||||||
assignCallback(texture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 检查纹理是否加载完成 */
|
|
||||||
private checkTextureLoadedWithPromise(texture: Texture): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (texture.isReady()) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
texture.onLoadObservable.addOnce(() => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置相机位置 */
|
|
||||||
reSet() {
|
|
||||||
if (this.mainApp.appCamera?.object?.position) {
|
|
||||||
this.mainApp.appCamera.object.position.set(160, 50, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 卷帘门开合:再次调用会反向动作 */
|
|
||||||
toggleRollerDoor(options?: RollerDoorOptions): void {
|
|
||||||
this.setRollerDoorState(!this.rollerDoorIsOpen, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 直接设置卷帘门状态 */
|
|
||||||
setRollerDoorState(open: boolean, options?: RollerDoorOptions): void {
|
|
||||||
const scene = this.mainApp.appScene?.object;
|
|
||||||
if (!scene) {
|
|
||||||
console.warn('Scene not found for roller door');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cacheRollerDoorMeshes(options?.meshNames);
|
|
||||||
|
|
||||||
if (!this.rollerDoorGroup || !this.rollerDoorMeshes.length) {
|
|
||||||
console.warn('Roller door group or meshes not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const speed = Math.max(options?.speed ?? 1, 0.01);
|
|
||||||
|
|
||||||
// 计算目标高度
|
|
||||||
let targetY: number;
|
|
||||||
if (open) {
|
|
||||||
// 上升时:如果指定了绝对高度就用绝对高度,否则用相对高度
|
|
||||||
if (options?.upY !== undefined) {
|
|
||||||
targetY = options.upY;
|
|
||||||
} else {
|
|
||||||
// 找到所有门中最高的初始位置,让所有门都升到这个高度+3
|
|
||||||
const maxBaseY = Math.max(...this.rollerDoorMeshes.map(m =>
|
|
||||||
this.rollerDoorInitialY.get(m.name) ?? m.position.y
|
|
||||||
));
|
|
||||||
targetY = maxBaseY + 3;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 下降时:回到初始位置
|
|
||||||
targetY = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已经在目标位置
|
|
||||||
if (Math.abs(this.rollerDoorGroup.position.y - targetY) < 0.001) {
|
|
||||||
this.rollerDoorIsOpen = open;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rollerDoorIsOpen = open;
|
|
||||||
this.stopRollerDoorAnimation();
|
|
||||||
|
|
||||||
this.rollerDoorObserver = scene.onBeforeRenderObservable.add(() => {
|
|
||||||
const dt = scene.getEngine().getDeltaTime() / 1000;
|
|
||||||
const current = this.rollerDoorGroup!.position.y;
|
|
||||||
const direction = targetY >= current ? 1 : -1;
|
|
||||||
|
|
||||||
// 使用固定速度变量
|
|
||||||
const step = speed * dt;
|
|
||||||
let next = current + direction * step;
|
|
||||||
|
|
||||||
if ((direction > 0 && next >= targetY) || (direction < 0 && next <= targetY)) {
|
|
||||||
next = targetY;
|
|
||||||
this.stopRollerDoorAnimation();
|
|
||||||
this.rollerDoorIsOpen = open;
|
|
||||||
console.log('Roller door animation finished');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动透明盒子
|
|
||||||
this.rollerDoorGroup!.position.y = next;
|
|
||||||
|
|
||||||
// 打印每个卷帘门的当前位置
|
|
||||||
// console.log('Roller door positions:');
|
|
||||||
// for (const mesh of this.rollerDoorMeshes) {
|
|
||||||
// console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 当前卷帘门是否开启 */
|
|
||||||
isRollerDoorOpen(): boolean {
|
|
||||||
return this.rollerDoorIsOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置卷帘门的缩放
|
|
||||||
* @param meshName - 卷帘门网格名称
|
|
||||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
|
||||||
*/
|
|
||||||
setRollerDoorScale(meshName: string, scale: number | Vector3): void {
|
|
||||||
const mesh = this.meshDic.Get(meshName);
|
|
||||||
if (mesh) {
|
|
||||||
if (typeof scale === 'number') {
|
|
||||||
mesh.scaling.set(scale, scale, scale);
|
|
||||||
} else {
|
|
||||||
mesh.scaling.copyFrom(scale);
|
|
||||||
}
|
|
||||||
console.log(`Set scale for ${meshName}:`, mesh.scaling.asArray());
|
|
||||||
} else {
|
|
||||||
console.warn(`Roller door mesh not found: ${meshName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置所有卷帘门的缩放
|
|
||||||
* @param scale - 缩放值(可以是单个数字或 Vector3)
|
|
||||||
*/
|
|
||||||
setAllRollerDoorsScale(scale: number | Vector3): void {
|
|
||||||
this.rollerDoorMeshes.forEach(mesh => {
|
|
||||||
if (typeof scale === 'number') {
|
|
||||||
mesh.scaling.set(scale, scale, scale);
|
|
||||||
} else {
|
|
||||||
mesh.scaling.copyFrom(scale);
|
|
||||||
}
|
|
||||||
console.log(`Set scale for ${mesh.name}:`, mesh.scaling.asArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置基于 Y 轴的剖切平面,keepAbove=true 时保留平面以上部分
|
|
||||||
* onlyMeshNames 指定只作用于哪些网格,其他网格不受影响
|
|
||||||
*/
|
|
||||||
setYAxisClip(
|
|
||||||
height: number,
|
|
||||||
keepAbove = true,
|
|
||||||
onlyMeshNames?: string[],
|
|
||||||
excludeMeshNames?: string[]
|
|
||||||
): void {
|
|
||||||
const scene = this.mainApp.appScene?.object;
|
|
||||||
if (!scene) {
|
|
||||||
console.warn('Scene not found for clipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const normal = new Vector3(0, keepAbove ? 1 : -1, 0);
|
|
||||||
this.yClipPlane = Plane.FromPositionAndNormal(new Vector3(0, height, 0), normal);
|
|
||||||
|
|
||||||
// 如果指定了特定网格,只对这些网格应用剖切
|
|
||||||
if (onlyMeshNames?.length) {
|
|
||||||
this.applyClipPlaneToMeshes(this.yClipPlane, onlyMeshNames);
|
|
||||||
} else {
|
|
||||||
// 否则使用场景级别的剖切,作用于所有网格
|
|
||||||
scene.clipPlane = this.yClipPlane;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[clipping] Scene clipPlane set:', { height, keepAbove, normal: normal.asArray(), targets: onlyMeshNames || 'all' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 关闭 Y 轴剖切 */
|
|
||||||
clearYAxisClip(): void {
|
|
||||||
const scene = this.mainApp.appScene?.object;
|
|
||||||
if (scene) {
|
|
||||||
scene.clipPlane = null;
|
|
||||||
}
|
|
||||||
this.yClipPlane = null;
|
|
||||||
this.yClipTargets = null;
|
|
||||||
|
|
||||||
// 清除所有网格材质上的 clipPlane
|
|
||||||
this.meshDic.Values().forEach((mesh) => {
|
|
||||||
const mat = mesh.material as any;
|
|
||||||
if (mat && 'clipPlane' in mat) {
|
|
||||||
mat.clipPlane = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private cacheRollerDoorMeshes(customNames?: string[]): void {
|
|
||||||
const scene = this.mainApp.appScene?.object;
|
|
||||||
if (!scene) return;
|
|
||||||
|
|
||||||
const names = customNames?.length ? customNames : this.rollerDoorNames;
|
|
||||||
this.rollerDoorMeshes = [];
|
|
||||||
|
|
||||||
// 创建或获取 group 作为父级
|
|
||||||
if (!this.rollerDoorGroup) {
|
|
||||||
// 创建一个 AbstractMesh 作为组
|
|
||||||
// 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化
|
|
||||||
this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any;
|
|
||||||
// 确保 group 的初始位置为 (0, 0, 0)
|
|
||||||
this.rollerDoorGroup.position.set(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const name of names) {
|
|
||||||
const mesh = this.meshDic.Get(name);
|
|
||||||
if (mesh) {
|
|
||||||
this.rollerDoorMeshes.push(mesh);
|
|
||||||
|
|
||||||
// 保存网格的当前位置作为初始位置
|
|
||||||
if (!this.rollerDoorInitialY.has(name)) {
|
|
||||||
this.rollerDoorInitialY.set(name, mesh.position.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存网格的世界位置和缩放
|
|
||||||
const worldPosition = mesh.getAbsolutePosition();
|
|
||||||
const worldScaling = new Vector3(mesh.scaling.x, mesh.scaling.y, mesh.scaling.z);
|
|
||||||
|
|
||||||
// 将网格添加到 group 中
|
|
||||||
mesh.parent = this.rollerDoorGroup;
|
|
||||||
|
|
||||||
// 调整网格的局部位置和缩放,保持世界位置和大小不变
|
|
||||||
mesh.setAbsolutePosition(worldPosition);
|
|
||||||
mesh.scaling.copyFrom(worldScaling);
|
|
||||||
} else {
|
|
||||||
console.warn(`Roller door mesh not found: ${name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private stopRollerDoorAnimation(): void {
|
|
||||||
const scene = this.mainApp.appScene?.object;
|
|
||||||
if (scene && this.rollerDoorObserver) {
|
|
||||||
scene.onBeforeRenderObservable.remove(this.rollerDoorObserver);
|
|
||||||
}
|
|
||||||
this.rollerDoorObserver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将 clipPlane 只作用到指定网格的材质 */
|
|
||||||
private applyClipPlaneToMeshes(plane: Plane, targetNames: string[]): void {
|
|
||||||
const targetSet = new Set(targetNames);
|
|
||||||
let appliedCount = 0;
|
|
||||||
|
|
||||||
this.meshDic.Values().forEach((mesh) => {
|
|
||||||
const mat = mesh.material as any;
|
|
||||||
if (!mat) {
|
|
||||||
console.log('[clipping] Mesh has no material:', mesh.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetSet.has(mesh.name)) {
|
|
||||||
// 目标网格:应用剖切
|
|
||||||
mat.clipPlane = plane;
|
|
||||||
appliedCount++;
|
|
||||||
console.log('[clipping] Applied to mesh:', mesh.name, 'material:', mat.name);
|
|
||||||
} else {
|
|
||||||
// 非目标网格:清除剖切
|
|
||||||
mat.clipPlane = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[clipping] Total meshes processed:', this.meshDic.Keys().length, 'Applied to:', appliedCount);
|
|
||||||
if (appliedCount === 0) {
|
|
||||||
console.warn('[clipping] No meshes found with names:', targetNames);
|
|
||||||
console.log('[clipping] Available mesh names:', this.meshDic.Keys());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取公共URL */
|
|
||||||
private getPublicUrl(): string {
|
|
||||||
// 尝试从环境变量获取
|
|
||||||
if (import.meta && import.meta.env && import.meta.env.VITE_PUBLIC_URL) {
|
|
||||||
return import.meta.env.VITE_PUBLIC_URL;
|
|
||||||
}
|
|
||||||
// 默认返回空字符串
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清理资源 */
|
|
||||||
dispose() {
|
|
||||||
this.stopRollerDoorAnimation();
|
|
||||||
this.clearYAxisClip();
|
|
||||||
this.rollerDoorMeshes = [];
|
|
||||||
this.rollerDoorInitialY.clear();
|
|
||||||
this.rollerDoorIsOpen = false;
|
|
||||||
|
|
||||||
// 清理 rollerDoorGroup
|
|
||||||
if (this.rollerDoorGroup && this.rollerDoorGroup.dispose) {
|
|
||||||
this.rollerDoorGroup.dispose();
|
|
||||||
this.rollerDoorGroup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理所有材质资源
|
|
||||||
this.materialDic.Values().forEach((material) => {
|
|
||||||
if (material && material.dispose) {
|
|
||||||
material.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.materialDic.Clear();
|
|
||||||
|
|
||||||
// 清理所有贴图资源
|
|
||||||
this.clearTextures(this.oldTextureDic);
|
|
||||||
|
|
||||||
// 清理所有网格
|
|
||||||
this.meshDic.Values().forEach((mesh) => {
|
|
||||||
if (mesh && mesh.dispose) {
|
|
||||||
mesh.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.meshDic.Clear();
|
|
||||||
|
|
||||||
// 清空失败贴图记录
|
|
||||||
this.failedTextures = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 更新 */
|
|
||||||
update() { }
|
|
||||||
|
|
||||||
/** 尝试创建贴图的方法,支持多种格式回退 */
|
|
||||||
private createTextureWithFallback(texturePath: string): Texture | null {
|
|
||||||
const failureReasons: string[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const texture = new Texture(texturePath);
|
|
||||||
|
|
||||||
if (texture) {
|
|
||||||
return texture;
|
|
||||||
} else {
|
|
||||||
failureReasons.push(`原始路径创建失败: ${texturePath}`);
|
|
||||||
throw new Error('Texture creation returned null');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMessage = error.message || error.toString();
|
|
||||||
|
|
||||||
// 特别处理KTX错误
|
|
||||||
if (errorMessage.includes('KTX identifier') || errorMessage.includes('missing KTX') ||
|
|
||||||
(texturePath.toLowerCase().endsWith('.ktx2') && errorMessage)) {
|
|
||||||
this.failedTextures.push({
|
|
||||||
path: texturePath,
|
|
||||||
textureType: 'KTX2',
|
|
||||||
error: `KTX错误: ${errorMessage}`,
|
|
||||||
timestamp: new Date()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
failureReasons.push(`原始路径加载异常: ${texturePath} - ${errorMessage}`);
|
|
||||||
|
|
||||||
// 如果是ktx2文件加载失败,尝试查找对应的jpg/png文件
|
|
||||||
if (texturePath.toLowerCase().endsWith('.ktx2')) {
|
|
||||||
// 尝试jpg格式
|
|
||||||
const jpgPath = texturePath.replace(/\.ktx2$/i, '.jpg');
|
|
||||||
try {
|
|
||||||
const jpgTexture = new Texture(jpgPath);
|
|
||||||
if (jpgTexture) {
|
|
||||||
return jpgTexture;
|
|
||||||
}
|
|
||||||
} catch (jpgError: any) {
|
|
||||||
failureReasons.push(`JPG回退失败: ${jpgPath} - ${jpgError}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试png格式
|
|
||||||
const pngPath = texturePath.replace(/\.ktx2$/i, '.png');
|
|
||||||
try {
|
|
||||||
const pngTexture = new Texture(pngPath);
|
|
||||||
if (pngTexture) {
|
|
||||||
return pngTexture;
|
|
||||||
}
|
|
||||||
} catch (pngError: any) {
|
|
||||||
failureReasons.push(`PNG回退失败: ${pngPath} - ${pngError}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有格式都失败,记录详细失败信息
|
|
||||||
this.failedTextures.push({
|
|
||||||
path: texturePath,
|
|
||||||
textureType: '回退机制',
|
|
||||||
error: failureReasons.join('; '),
|
|
||||||
timestamp: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用材质属性
|
* 应用材质属性
|
||||||
* @param options 材质配置选项
|
* @param options 材质配置选项
|
||||||
@ -761,12 +77,10 @@ export class GameManager extends Monobehiver {
|
|||||||
}): void {
|
}): void {
|
||||||
this.updateDictionaries();
|
this.updateDictionaries();
|
||||||
|
|
||||||
// 查找目标材质(支持精确匹配和前缀匹配)
|
|
||||||
const targetMaterials: PBRMaterial[] = [];
|
const targetMaterials: PBRMaterial[] = [];
|
||||||
|
|
||||||
// 如果提供了 modelId,只查找该模型的材质
|
// 如果提供了 modelId,只查找该模型的材质
|
||||||
if (options.modelId) {
|
if (options.modelId) {
|
||||||
// 获取该模型的所有 meshes
|
|
||||||
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
const modelMeshes = this.mainApp.appModel.modelDic.Get(options.modelId);
|
||||||
|
|
||||||
if (!modelMeshes || modelMeshes.length === 0) {
|
if (!modelMeshes || modelMeshes.length === 0) {
|
||||||
@ -774,12 +88,10 @@ export class GameManager extends Monobehiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遍历该模型的所有 mesh,查找匹配的材质
|
|
||||||
modelMeshes.forEach((mesh: AbstractMesh) => {
|
modelMeshes.forEach((mesh: AbstractMesh) => {
|
||||||
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
if (mesh.material && mesh.material instanceof PBRMaterial) {
|
||||||
const material = mesh.material as PBRMaterial;
|
const material = mesh.material as PBRMaterial;
|
||||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||||
// 避免重复添加
|
|
||||||
if (!targetMaterials.includes(material)) {
|
if (!targetMaterials.includes(material)) {
|
||||||
targetMaterials.push(material);
|
targetMaterials.push(material);
|
||||||
}
|
}
|
||||||
@ -787,11 +99,9 @@ export class GameManager extends Monobehiver {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// 没有提供 modelId,全局查找
|
||||||
// 没有提供 modelId,全局查找(保持向后兼容)
|
|
||||||
this.materialDic.Values().forEach(material => {
|
this.materialDic.Values().forEach(material => {
|
||||||
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
|
||||||
console.log(material.name);
|
|
||||||
targetMaterials.push(material);
|
targetMaterials.push(material);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -801,8 +111,8 @@ export class GameManager extends Monobehiver {
|
|||||||
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
console.warn(`Material not found: ${options.target}${options.modelId ? ` in model ${options.modelId}` : ''}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(options);
|
|
||||||
// 应用材质属性到目标材质
|
// 应用材质属性
|
||||||
targetMaterials.forEach(material => {
|
targetMaterials.forEach(material => {
|
||||||
// 应用颜色
|
// 应用颜色
|
||||||
if (options.albedoColor) {
|
if (options.albedoColor) {
|
||||||
@ -810,51 +120,47 @@ export class GameManager extends Monobehiver {
|
|||||||
material.albedoColor.copyFrom(color);
|
material.albedoColor.copyFrom(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用反照率纹理(颜色贴图)
|
// 应用反照率纹理
|
||||||
if (options.albedoTexture !== undefined) {
|
if (options.albedoTexture !== undefined) {
|
||||||
if (options.albedoTexture) {
|
if (options.albedoTexture) {
|
||||||
material.albedoTexture = new Texture(options.albedoTexture);
|
material.albedoTexture = new Texture(options.albedoTexture, this.mainApp.appScene.object);
|
||||||
} else {
|
} else {
|
||||||
// 传入空字符串或 null 时清空贴图
|
|
||||||
material.albedoTexture = null;
|
material.albedoTexture = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 应用法线贴图
|
// 应用法线贴图
|
||||||
// if (options.normalMap !== undefined) {
|
if (options.normalMap !== undefined) {
|
||||||
// if (options.normalMap) {
|
if (options.normalMap) {
|
||||||
// material.bumpTexture = new Texture(options.normalMap);
|
material.bumpTexture = new Texture(options.normalMap, this.mainApp.appScene.object);
|
||||||
// } else {
|
} else {
|
||||||
// // 传入空字符串或 null 时清空贴图
|
material.bumpTexture = null;
|
||||||
// material.bumpTexture = null;
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// // 应用金属度贴图
|
// 应用金属度贴图
|
||||||
// if (options.metallicTexture !== undefined) {
|
if (options.metallicTexture !== undefined) {
|
||||||
// if (options.metallicTexture) {
|
if (options.metallicTexture) {
|
||||||
// material.metallicTexture = new Texture(options.metallicTexture);
|
material.metallicTexture = new Texture(options.metallicTexture, this.mainApp.appScene.object);
|
||||||
// } else {
|
} else {
|
||||||
// // 传入空字符串或 null 时清空贴图
|
material.metallicTexture = null;
|
||||||
// material.metallicTexture = null;
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// 应用粗糙度值
|
// 应用粗糙度
|
||||||
// if (options.roughness !== undefined) {
|
if (options.roughness !== undefined) {
|
||||||
// material.roughness = options.roughness;
|
if (material.roughness !== options.roughness) {
|
||||||
// }
|
material.roughness = options.roughness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // 应用金属度值
|
// 应用金属度
|
||||||
// if (options.metallic !== undefined) {
|
if (options.metallic !== undefined) {
|
||||||
// material.metallic = options.metallic;
|
if (material.metallic !== options.metallic) {
|
||||||
// }
|
material.metallic = options.metallic;
|
||||||
// alert(typeof options.metallic + ' ' + typeof options.roughness);
|
}
|
||||||
|
}
|
||||||
// 强制刷新材质
|
|
||||||
material.markDirty();
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export class MainApp {
|
|||||||
AppConfig.container = config.container;
|
AppConfig.container = config.container;
|
||||||
AppConfig.modelUrlList = config.modelUrlList || [];
|
AppConfig.modelUrlList = config.modelUrlList || [];
|
||||||
AppConfig.env = { ...AppConfig.env, ...(config.env || {}) };
|
AppConfig.env = { ...AppConfig.env, ...(config.env || {}) };
|
||||||
|
AppConfig.camera = { ...AppConfig.camera, ...(config.camera || {}) };
|
||||||
AppConfig.gizmo = { ...AppConfig.gizmo, ...(config.gizmo || {}) };
|
AppConfig.gizmo = { ...AppConfig.gizmo, ...(config.gizmo || {}) };
|
||||||
AppConfig.outline = { ...AppConfig.outline, ...(config.outline || {}) };
|
AppConfig.outline = { ...AppConfig.outline, ...(config.outline || {}) };
|
||||||
this.appPositionGizmo.configure(AppConfig.gizmo);
|
this.appPositionGizmo.configure(AppConfig.gizmo);
|
||||||
|
|||||||
@ -118,33 +118,33 @@ export class KernelAdapter {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 卷帘门控<E997A8>?*/
|
// /** 卷帘门控<E997A8>?*/
|
||||||
door = {
|
// door = {
|
||||||
/** 再次调用会自动反向动<E59091>?*/
|
// /** 再次调用会自动反向动<E59091>?*/
|
||||||
toggle: (options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
|
// toggle: (options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
|
||||||
this.mainApp.gameManager.toggleRollerDoor(options);
|
// this.mainApp.gameManager.toggleRollerDoor(options);
|
||||||
},
|
// },
|
||||||
/** 显式设置开/<2F>?*/
|
// /** 显式设置开/<2F>?*/
|
||||||
setState: (open: boolean, options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
|
// setState: (open: boolean, options?: { upY?: number; downY?: number; speed?: number; meshNames?: string[] }): void => {
|
||||||
this.mainApp.gameManager.setRollerDoorState(open, options);
|
// this.mainApp.gameManager.setRollerDoorState(open, options);
|
||||||
},
|
// },
|
||||||
/** 当前是否已开<E5B7B2>?*/
|
// /** 当前是否已开<E5B7B2>?*/
|
||||||
isOpen: (): boolean => {
|
// isOpen: (): boolean => {
|
||||||
return this.mainApp.gameManager.isRollerDoorOpen();
|
// return this.mainApp.gameManager.isRollerDoorOpen();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
/** Y 轴剖<E8BDB4>?*/
|
// /** Y 轴剖<E8BDB4>?*/
|
||||||
clipping = {
|
// clipping = {
|
||||||
/** <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>и߶ȣ<DFB6>keepAbove=true ʱ<><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϲ<EFBFBD><CFB2>֣<EFBFBD>onlyMeshNames Ϊ<><CEAA>Ĭ<EFBFBD>Ͻ<EFBFBD><CFBD><EFBFBD><EFBFBD>þ<EFBFBD><C3BE><EFBFBD><EFBFBD>ţ<EFBFBD>excludeMeshNames <20><><EFBFBD><EFBFBD><EFBFBD>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD><D0B5><EFBFBD><EFBFBD><EFBFBD> */
|
// /** <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>и߶ȣ<DFB6>keepAbove=true ʱ<><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϲ<EFBFBD><CFB2>֣<EFBFBD>onlyMeshNames Ϊ<><CEAA>Ĭ<EFBFBD>Ͻ<EFBFBD><CFBD><EFBFBD><EFBFBD>þ<EFBFBD><C3BE><EFBFBD><EFBFBD>ţ<EFBFBD>excludeMeshNames <20><><EFBFBD><EFBFBD><EFBFBD>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD><D0B5><EFBFBD><EFBFBD><EFBFBD> */
|
||||||
setY: (height: number, keepAbove = true, onlyMeshNames?: string[], excludeMeshNames?: string[]): void => {
|
// setY: (height: number, keepAbove = true, onlyMeshNames?: string[], excludeMeshNames?: string[]): void => {
|
||||||
this.mainApp.gameManager.setYAxisClip(height, keepAbove, onlyMeshNames, excludeMeshNames);
|
// this.mainApp.gameManager.setYAxisClip(height, keepAbove, onlyMeshNames, excludeMeshNames);
|
||||||
},
|
// },
|
||||||
/** 关闭剖切 */
|
// /** 关闭剖切 */
|
||||||
clear: (): void => {
|
// clear: (): void => {
|
||||||
this.mainApp.gameManager.clearYAxisClip();
|
// this.mainApp.gameManager.clearYAxisClip();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
/** 热点管理 */
|
/** 热点管理 */
|
||||||
hotspot = {
|
hotspot = {
|
||||||
@ -385,6 +385,12 @@ export class KernelAdapter {
|
|||||||
hide: (): void => {
|
hide: (): void => {
|
||||||
this.mainApp.appDropZone.hide();
|
this.mainApp.appDropZone.hide();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 只显示指定墙面的放置区域
|
||||||
|
*/
|
||||||
|
showWall: (wallName: string): void => {
|
||||||
|
this.mainApp.appDropZone.showWall(wallName);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 清除所有放置区域(只清除网格,不清除模型)
|
* 清除所有放置区域(只清除网格,不清除模型)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -23,6 +23,10 @@ type InitParams = {
|
|||||||
rotationY?: number;
|
rotationY?: number;
|
||||||
background?: boolean;
|
background?: boolean;
|
||||||
};
|
};
|
||||||
|
camera?: {
|
||||||
|
position?: { x: number; y: number; z: number };
|
||||||
|
target?: { x: number; y: number; z: number };
|
||||||
|
};
|
||||||
gizmo?: {
|
gizmo?: {
|
||||||
position?: boolean;
|
position?: boolean;
|
||||||
rotation?: boolean;
|
rotation?: boolean;
|
||||||
@ -66,6 +70,7 @@ const kernel = {
|
|||||||
container,
|
container,
|
||||||
modelUrlList: params.modelUrlList || [],
|
modelUrlList: params.modelUrlList || [],
|
||||||
env: params.env,
|
env: params.env,
|
||||||
|
camera: params.camera,
|
||||||
gizmo: params.gizmo,
|
gizmo: params.gizmo,
|
||||||
outline: params.outline,
|
outline: params.outline,
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.3 KiB |