Compare commits

...

8 Commits

Author SHA1 Message Date
09cd8072b8 1 2026-06-05 10:05:47 +08:00
1a518ce04f 1 2026-06-05 10:05:32 +08:00
6e0fefd3c9 优化Gamemanager 2026-06-04 19:31:40 +08:00
14287777a4 修复拖拽逻辑 2026-06-04 19:05:53 +08:00
2abb61104a 修改粗糙金属度 2026-06-04 19:01:53 +08:00
bef0bf527b 1 2026-06-04 16:15:21 +08:00
7676364229 1 2026-05-29 14:25:03 +08:00
48456acd3d 1 2026-05-29 14:24:59 +08:00
47 changed files with 362401 additions and 2150 deletions

View File

@ -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 *)"
]
}
}

19
.gitignore vendored
View File

@ -1,5 +1,14 @@
/node_modules/
/public/
/dist/
/assets/
nul
# 大文件忽略
*.zip
*.rar
*.7z
*.glb
*.gltf
*.png
*.jpg
*.jpeg
node_modules/
dist/
build/
.DS_Store
*.log

View File

@ -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 = 原始大小)
- 可以设置不同轴向的缩放比例

View 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
View 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% |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -53,6 +53,7 @@ const init = async (customConfig = {}) => {
// 合并用户自定义配置
const config = { ...defaultConfig, ...customConfig };
kernel.init(config);
await getAutoLoadModelList()
}
//初始化加载模型
@ -97,10 +98,14 @@ const getPlacementZone = async (sku) => {
let division_include = []
// 同时包含10和13
const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku)
// 只包含10 无13 无12
const only10 = /(?=.*10)(?!.*13)(?!.*12)/.test(pergolaSku)
// 只包含10 无13 无12 无20
const only10 = /(?=.*10)(?!.*13)(?!.*12)(?!.*20)/.test(pergolaSku)
// 同时包含10和12
const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku)
// 同时包含10和20
const only10_20 = /(?=.*10)(?=.*20)/.test(pergolaSku)
// 只包含13 无13 无12 无20
const only13 = /(?=.*13)(?!.*10)(?!.*12)(?!.*20)/.test(pergolaSku)
// 1. 只要字符串里包含 10就返回 true
const has10 = /10/.test(sku);
@ -131,6 +136,32 @@ const getPlacementZone = async (sku) => {
if (only10_12 && has10) {
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('前', '后')
}
}
//棚子只包13的并且含配件是13
if (pergolaSku === "SPF111SEM13" && has13) {
division_include.push('前', '后')
}
if (pergolaSku === "SPF111SEM13" && has10) {
division_include.push('左', '右')
}
const response = await fetch(getApiUrl(`/api/product-configs/by-sku/${sku}`));
const result = await response.json();
@ -209,6 +240,10 @@ const executeEvent = async (dropzone_data, result, sku) => {
enable: true,
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
step: 0.1,
snapToZone: true, // 拖拽吸附到最近的分割区域
returnWhenOutOfBounds: true, // 拖拽到区域外时返回原位置
handleOccupiedZone: true, // 处理已占用区域false=允许重叠)
occupiedZoneAction: 'replace' // 当开关3=true时的行为'return'=返回原位置,'replace'=替换
},
transform: {
position: position,
@ -336,14 +371,16 @@ const isModelExists = (modelId) => {
for (const event of result.data.events) {
if (event.event_type === 'change_color') {
const materialName = event.material_name;
console.log('替换模型颜色:', event);
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
console.log('替换模型颜色:', event.target_data);
kernel.material.apply({
target: materialName,
albedoColor: color,
albedoTexture: color_map_url,
normalMap: normal_map_url,
});
console.log(`百叶模型颜色已替换为 ${color}`);
@ -419,7 +456,7 @@ const getProductConfig = async (sku) => {
// API 配置
const API_BASE_URL = 'https://ztserver.zguiy.com';
//const API_BASE_URL = 'http://localhost:26517';
const getApiUrl = (path) => {
return `${API_BASE_URL}${path}`;
};

View File

@ -366,7 +366,7 @@
<div class="option-group">
<button class="option-btn" data-option="size-1">SPF111S1013W</button>
<button class="option-btn" data-option="size-2">SPF111S1013TA</button>
<button class="option-btn" data-option="size-3">SPF111S1013C</button>
<button class="option-btn" data-option="size-3">SPF111SEM13</button>
</div>
</div>
</div>
@ -410,13 +410,20 @@
<!-- 80 系列 -->
<div class="series-divider">----- 80 -----</div>
<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>
<!-- 111 系列 -->
<div class="series-divider">----- 111 -----</div>
<div class="option-group">
<button class="option-btn" data-option="size-7">SPF111S1020C</button>
</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>
@ -452,8 +459,8 @@
</div>
<div class="category-content expanded">
<div class="option-group">
<button class="option-btn" data-option="color-1">SPFPDS13FTW</button>
<button class="option-btn" data-option="color-2">SPFPDS13FTC</button>
<button class="option-btn" data-option="color-1">SPFFLOW13FTC</button>
<button class="option-btn" data-option="color-2">SPFGLASS13FT</button>
</div>
</div>
</div>
@ -466,8 +473,8 @@
</div>
<div class="category-content expanded">
<div class="option-group">
<button class="option-btn" data-option="color-3">SPFPDS10FTW</button>
<button class="option-btn" data-option="color-4">SPFPDS10FTC</button>
<button class="option-btn" data-option="color-3">SPF80CS10FTS</button>
<button class="option-btn" data-option="color-4">SPFGLASS10FT</button>
</div>
</div>
</div>
@ -579,6 +586,7 @@
</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="./app-global.js"></script>
<script>
// 从全局对象获取 SDK kernel
@ -870,10 +878,6 @@
console.log(kernel);
// 监听放置区域点击事件
kernel.on('dropzone:click', function (dropzone_data) {
window.AppLogic.getEvent(dropzone_data, sku);
});
// 存储当前选中的材质名和网格
var currentMaterialName = '';

View File

@ -221,33 +221,40 @@
cursor: pointer;
}
/* 进度条样式 */
/* 加载遮罩样式 */
#progress-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 10px;
z-index: 1000;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
}
#progress-bar {
width: 0%;
height: 10px;
background: linear-gradient(90deg, #4CAF50, #45a049);
border-radius: 5px;
transition: width 0.1s ease;
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
}
#progress-text {
color: white;
text-align: center;
margin-top: 5px;
font-size: 14px;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
color: #fff;
font-size: 16px;
margin-top: 20px;
}
</style>
</head>
@ -258,8 +265,8 @@
<div id="canvas-container">
<canvas id="renderDom"></canvas>
<div id="progress-container" style="display: none;">
<div id="progress-bar"></div>
<div id="progress-text">0%</div>
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<!-- 生成放置区域按钮 -->
@ -847,9 +854,25 @@
// 监听放置区域点击事件
kernel.on('dropzone:click', async (dropzone_data) => {
// 显示进度条
const progressContainer = document.getElementById('progress-container');
if (progressContainer) {
progressContainer.style.display = 'flex';
}
getEvent(dropzone_data, sku)
});
// 监听模型加载完成事件
kernel.on('model:loaded', (data) => {
console.log('模型加载完成', data);
// 隐藏进度条
const progressContainer = document.getElementById('progress-container');
if (progressContainer) {
progressContainer.style.display = 'none';
}
});

View File

@ -29855,6 +29855,10 @@ var kernel = (function (exports) {
rotationY: 0,
background: false
},
camera: {
position: { x: 0, y: 2, z: 5 },
target: { x: 0, y: 1, z: 0 }
},
gizmo: {
position: true,
rotation: false,
@ -70348,14 +70352,22 @@ var kernel = (function (exports) {
const scene = this.mainApp.appScene.object;
const canvas = AppConfig.container;
if (!scene || !canvas) return;
this.object = new ArcRotateCamera("Camera", Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
const { position, target } = AppConfig.camera;
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.minZ = 0.01;
this.object.wheelPrecision = 200;
this.object.panningSensibility = 0;
this.object.upperBetaLimit = Tools.ToRadians(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);
}
/** 设置相机目标点 */
setTarget(x, y, z) {
@ -79877,10 +79889,8 @@ discard;}}
}
/** 初始化灯光并开启阴影 */
Awake() {
new DirectionalLight(
"mainLight",
new Vector3(0, -0.5, -1)
);
const light = new DirectionalLight("sun", new Vector3(-1, -2, -1), this.mainApp.appScene.object);
light.intensity = 1;
}
}
@ -354680,16 +354690,6 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
});
return min.add(max).scaleInPlace(0.5);
}
setupShadows(meshes) {
const appLight = this.mainApp.appLight;
if (!appLight) return;
meshes.forEach((mesh) => {
if (mesh.getTotalVertices() > 0) {
appLight.addShadowCaster(mesh);
mesh.receiveShadows = true;
}
});
}
/** 获取缓存的网格 */
getCachedMeshes(name) {
return this.modelDic.Get(name);
@ -354748,6 +354748,22 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
if (drag) {
this.mainApp.appModelDrag?.configureDrag(modelName + "_" + modelId, drag);
if (this.mainApp.appDropZone) {
let modelWallName = null;
const fullModelId = modelName + "_" + modelId;
this.mainApp.appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
if (id === fullModelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
modelWallName = match[1];
}
}
});
if (modelWallName && this.mainApp.appDropZone.isWallFull(modelWallName)) {
console.log(`[拖拽控制] 墙面 ${modelWallName} 已满,禁用模型 ${fullModelId} 的拖拽`);
this.mainApp.appModelDrag?.setDragEnabled(fullModelId, false);
}
}
}
this.mainApp.gameManager?.updateDictionaries();
EventBridge.modelLoaded({ urls: [modelUrl] });
@ -354907,6 +354923,9 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
this.modelDic.Remove(modelName);
this.modelMetadataDic.Remove(modelName);
this.mainApp.gameManager?.updateDictionaries();
if (this.mainApp.appDropZone && typeof this.mainApp.appDropZone.notifyModelRemoved === "function") {
this.mainApp.appDropZone.notifyModelRemoved(modelName);
}
return true;
}
/**
@ -355067,9 +355086,10 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
* 将模型放置到指定的放置区域
* @param modelId 模型ID
* @param zoneInfo 放置区域信息
* @param offsetDistance 距离墙面的偏移距离默认0.1正数向外
* @param offsetDistance 距离墙面的偏移距离默认0正数向外
*/
placeToZone(modelId, zoneInfo, offsetDistance = 0) {
console.log(zoneInfo);
const meshes = this.modelDic.Get(modelId);
if (!meshes?.length) {
console.warn(`Model not found: ${modelId}`);
@ -355119,6 +355139,12 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
highlightLayer = null;
originalMaterial = null;
highlightedMesh = null;
pointerDownTime = 0;
pointerDownPickInfo = null;
longPressTimer = null;
longPressThreshold = 500;
// 长按阈值(毫秒)
isLongPress = false;
constructor(mainApp) {
super(mainApp);
}
@ -355143,29 +355169,85 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
if (type === PointerEventTypes.POINTERDOWN) {
this.oldPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
this.pointerDownTime = Date.now();
this.pointerDownPickInfo = pickInfo;
this.isLongPress = false;
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
this.longPressTimer = setTimeout(() => {
this.isLongPress = true;
this.handleLongPress(pointerEvent, this.pointerDownPickInfo);
}, this.longPressThreshold);
} else if (type === PointerEventTypes.POINTERUP) {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
const distance = Vector3.Distance(this.oldPoint, this.newPoint);
if (distance < 5) {
if (this.isLongPress) {
console.log("[长按] 松手,隐藏分割区域");
this.mainApp.appDropZone.hide();
}
if (distance < 5 && !this.isLongPress) {
this.handleSingleClick(pointerEvent, pickInfo);
}
this.isLongPress = false;
} else if (type === PointerEventTypes.POINTERMOVE) {
this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
const distance = Vector3.Distance(this.oldPoint, this.newPoint);
if (distance > 5 && this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
}
});
}
// 处理长按
handleLongPress(evt, pickInfo) {
if (pickInfo && pickInfo.hit && pickInfo.pickedMesh) {
if (pickInfo.pickedMesh.metadata?.type === "hotspot") {
return;
}
if (pickInfo.pickedMesh.name.startsWith("placement_")) {
return;
}
const modelName = this.mainApp.appModel.findModelNameByMesh(pickInfo.pickedMesh);
if (modelName) {
const wallName = this.findModelWallName(modelName);
if (wallName) {
console.log(`[长按] 模型 ${modelName} 位于墙面 ${wallName},显示该墙面的放置区域`);
this.mainApp.appDropZone.showWall(wallName);
}
}
}
}
// 查找模型所在的墙面
findModelWallName(modelId) {
const zoneModelMap = this.mainApp.appDropZone["zoneModelMap"];
if (!zoneModelMap) return null;
for (const [zoneKey, id] of zoneModelMap.entries()) {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
return match[1];
}
}
}
return null;
}
// 处理单击
handleSingleClick(evt, pickInfo) {
if (pickInfo && pickInfo.hit && pickInfo.pickedMesh && pickInfo.pickedPoint) {
if (pickInfo.pickedMesh.metadata?.type === "hotspot") {
return;
}
if (pickInfo.pickedMesh.name === "gridGround" || pickInfo.pickedMesh.name === "ground" || pickInfo.pickedMesh.name === "sphere_yundong_mesh") {
return;
}
if (pickInfo.pickedMesh.name.startsWith("placement_")) {
const zones = this.mainApp.appDropZone.getPlacementZones();
const clickedZone = zones.find((zone) => zone.mesh === pickInfo.pickedMesh);
if (clickedZone) {
const offsetDistance = 0.05;
const offsetDistance = 0;
const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance));
const targetDirection = clickedZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
@ -355186,7 +355268,6 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
rotation: {
x: 0,
y: angle * 180 / Math.PI,
// 转换为角度
z: 0
},
scale: {
@ -355200,6 +355281,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
}
this.mainApp.appDomTo3D.hideAll();
this.mainApp.appDropZone.hide();
const materialName = pickInfo.pickedMesh.material?.name || "";
const holdingShift = Boolean(evt.shiftKey);
const modelMeshes = this.mainApp.appModel.getModelMeshesByMesh(pickInfo.pickedMesh);
@ -355222,7 +355304,6 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
modelControlType: modelMetadata?.modelControlType
});
} else {
console.log(1111);
this.mainApp.appSelectionOutline.clear();
this.mainApp.appPositionGizmo.detach();
this.mainApp.appDomTo3D.hideAll();
@ -355877,6 +355958,8 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
material.albedoTexture = null;
}
}
material.roughness = 0.8;
material.metallic = 0;
material.markDirty();
});
}
@ -356535,11 +356618,44 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
const pointerDragBehavior = new PointerDragBehavior({ dragAxis });
pointerDragBehavior.useObjectOrientationForDragging = false;
let dragStartPosition = null;
let hasShownZones = false;
pointerDragBehavior.onDragStartObservable.add(() => {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (meshes && meshes.length > 0) {
dragStartPosition = meshes[0].position.clone();
}
this.disableCameraControl();
hasShownZones = false;
});
pointerDragBehavior.onDragObservable.add((event) => {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (meshes && meshes.length > 0 && dragStartPosition) {
const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
if (distance > 0.01 && !hasShownZones && dragInfo.config.snapToZone) {
this.showZonesForModel(modelId);
hasShownZones = true;
}
}
});
pointerDragBehavior.onDragEndObservable.add(() => {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
let hasMoved = false;
if (meshes && meshes.length > 0 && dragStartPosition) {
const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
hasMoved = distance > 0.01;
}
this.enableCameraControl();
if (hasMoved) {
if (dragInfo.config.snapToZone && hasShownZones) {
this.hideZonesForModel(modelId);
this.snapModelToZone(modelId);
} else {
this.updateModelZoneMapping(modelId);
}
}
dragStartPosition = null;
hasShownZones = false;
});
return pointerDragBehavior;
}
@ -356632,6 +356748,259 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
camera.attachControl(canvas, true);
}
}
/**
* 显示模型所在墙面的分割区域
* @param modelId 模型ID
*/
showZonesForModel(modelId) {
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
let wallName = null;
appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
wallName = match[1];
}
}
});
if (wallName) {
console.log(`[拖拽吸附] 显示墙面 ${wallName} 的分割区域`);
appDropZone.showWall(wallName);
}
}
/**
* 隐藏分割区域
* @param modelId 模型ID
*/
hideZonesForModel(modelId) {
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
console.log(`[拖拽吸附] 隐藏分割区域`);
appDropZone.hide();
}
/**
* 将模型吸附到最近的分割区域
* @param modelId 模型ID
*/
snapModelToZone(modelId) {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) return;
const rootMesh = meshes[0];
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
let wallName = null;
let originalZoneIndex = -1;
appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
wallName = match[1];
originalZoneIndex = parseInt(match[2]);
}
}
});
if (!wallName) return;
const wallZones = appDropZone.getZonesByWall(wallName);
if (!wallZones.length) return;
let closestZoneIndex = -1;
let minDistance = Number.POSITIVE_INFINITY;
wallZones.forEach((zone, index) => {
const distance = rootMesh.position.subtract(zone.center).length();
if (distance < minDistance) {
minDistance = distance;
closestZoneIndex = index;
}
});
if (closestZoneIndex === -1) return;
const dragInfo = this.modelDragMap.get(modelId);
const returnWhenOutOfBounds = dragInfo?.config.returnWhenOutOfBounds ?? false;
const handleOccupiedZone = dragInfo?.config.handleOccupiedZone ?? false;
const occupiedZoneAction = dragInfo?.config.occupiedZoneAction ?? "return";
let isOutOfBounds = false;
let minX = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let minZ = Number.POSITIVE_INFINITY;
let maxZ = Number.NEGATIVE_INFINITY;
wallZones.forEach((zone) => {
const halfWidth = zone.width / 2;
if (Math.abs(zone.normal.x) > 0.5) {
minZ = Math.min(minZ, zone.center.z - halfWidth);
maxZ = Math.max(maxZ, zone.center.z + halfWidth);
} else if (Math.abs(zone.normal.z) > 0.5) {
minX = Math.min(minX, zone.center.x - halfWidth);
maxX = Math.max(maxX, zone.center.x + halfWidth);
}
});
const currentPos = rootMesh.position;
if (minX !== Number.POSITIVE_INFINITY && maxX !== Number.NEGATIVE_INFINITY) {
if (currentPos.x < minX || currentPos.x > maxX) {
isOutOfBounds = true;
}
}
if (minZ !== Number.POSITIVE_INFINITY && maxZ !== Number.NEGATIVE_INFINITY) {
if (currentPos.z < minZ || currentPos.z > maxZ) {
isOutOfBounds = true;
}
}
if (isOutOfBounds) {
console.log(`[拖拽吸附] 模型 ${modelId} 超出边界`);
if (returnWhenOutOfBounds) {
console.log(`[拖拽吸附] 启用边界返回,回到原区域 ${originalZoneIndex}`);
if (originalZoneIndex !== -1) {
const originalZone = wallZones[originalZoneIndex];
if (originalZone) {
const offsetDistance2 = 0;
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance2));
rootMesh.position.copyFrom(returnPosition);
const targetDirection2 = originalZone.normal.scale(-1);
const angle2 = Math.atan2(targetDirection2.x, targetDirection2.z);
rootMesh.rotation.y = angle2;
console.log(`[拖拽吸附] 模型 ${modelId} 已返回原区域 ${originalZoneIndex}`);
return;
}
}
} else {
console.log(`[拖拽吸附] 未启用边界返回,保持当前位置,不做吸附`);
this.updateModelZoneMapping(modelId);
return;
}
}
const targetZone = wallZones[closestZoneIndex];
const targetZoneKey = `${wallName}[${closestZoneIndex}]`;
const occupyingModelId = appDropZone["zoneModelMap"]?.get(targetZoneKey);
if (occupyingModelId && occupyingModelId !== modelId) {
console.log(`[拖拽吸附] 目标区域 ${closestZoneIndex} 已被模型 ${occupyingModelId} 占用`);
if (handleOccupiedZone) {
if (occupiedZoneAction === "return") {
console.log(`[拖拽吸附] 配置为返回原位置,回到区域 ${originalZoneIndex}`);
if (originalZoneIndex !== -1) {
const originalZone = wallZones[originalZoneIndex];
if (originalZone) {
const offsetDistance2 = 0;
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance2));
rootMesh.position.copyFrom(returnPosition);
const targetDirection2 = originalZone.normal.scale(-1);
const angle2 = Math.atan2(targetDirection2.x, targetDirection2.z);
rootMesh.rotation.y = angle2;
console.log(`[拖拽吸附] 模型 ${modelId} 返回原区域 ${originalZoneIndex}`);
return;
}
}
} else if (occupiedZoneAction === "replace") {
console.log(`[拖拽吸附] 配置为替换模型,将替换模型 ${occupyingModelId}`);
}
} else {
console.log(`[拖拽吸附] 未启用占用区域处理,允许重叠`);
}
}
const offsetDistance = 0;
const snapPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
rootMesh.position.copyFrom(snapPosition);
const targetDirection = targetZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
rootMesh.rotation.y = angle;
console.log(`[拖拽吸附] 模型 ${modelId} 吸附到区域 ${closestZoneIndex}`);
this.updateModelZoneMapping(modelId);
}
/**
* 更新模型所属的分割区域映射
* @param modelId 模型ID
*/
updateModelZoneMapping(modelId) {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) return;
const rootMesh = meshes[0];
const modelPosition = rootMesh.position;
console.log(`[边界检测] 模型 ${modelId} 拖拽结束,当前位置:`, modelPosition);
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
let originalWallName = null;
appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
originalWallName = match[1];
}
}
});
if (!originalWallName) {
console.log(`[边界检测] 模型 ${modelId} 未找到原始墙面,跳过检测`);
return;
}
console.log(`[边界检测] 模型 ${modelId} 原始墙面: ${originalWallName}`);
const wallZones = appDropZone.getZonesByWall(originalWallName);
if (!wallZones.length) return;
console.log(`[边界检测] 墙面 ${originalWallName}${wallZones.length} 个分割区域`);
let closestZoneIndex = -1;
let minDistance = Number.POSITIVE_INFINITY;
wallZones.forEach((zone, index) => {
const distance = modelPosition.subtract(zone.center).length();
console.log(`[边界检测] 区域 ${index} 中心:`, zone.center, `距离: ${distance.toFixed(3)}`);
if (distance < minDistance) {
minDistance = distance;
closestZoneIndex = index;
}
});
if (closestZoneIndex === -1) {
console.log(`[边界检测] 未找到最近的区域`);
return;
}
console.log(`[边界检测] 模型 ${modelId} 最接近区域 ${closestZoneIndex},距离: ${minDistance.toFixed(3)}`);
let currentZoneIndex = -1;
appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
if (id === modelId) {
const match = zoneKey.match(/^.+\[(\d+)\]$/);
if (match) {
currentZoneIndex = parseInt(match[1]);
}
}
});
if (currentZoneIndex !== closestZoneIndex) {
console.log(`[边界检测] 模型 ${modelId} 从区域 ${currentZoneIndex} 移动到区域 ${closestZoneIndex}`);
if (currentZoneIndex !== -1) {
const oldKey = `${originalWallName}[${currentZoneIndex}]`;
appDropZone["zoneModelMap"]?.delete(oldKey);
console.log(`[边界检测] 删除旧映射: ${oldKey}`);
}
const newKey = `${originalWallName}[${closestZoneIndex}]`;
const existingModelId = appDropZone["zoneModelMap"]?.get(newKey);
const dragInfo = this.modelDragMap.get(modelId);
const handleOccupiedZone = dragInfo?.config.handleOccupiedZone ?? false;
const occupiedZoneAction = dragInfo?.config.occupiedZoneAction ?? "return";
if (existingModelId && existingModelId !== modelId) {
console.log(`[边界检测] 目标区域 ${closestZoneIndex} 已有模型 ${existingModelId}`);
if (handleOccupiedZone && occupiedZoneAction === "replace") {
console.log(`[边界检测] 配置为替换模式,交换位置`);
if (currentZoneIndex !== -1) {
const swapKey = `${originalWallName}[${currentZoneIndex}]`;
appDropZone["zoneModelMap"]?.set(swapKey, existingModelId);
console.log(`[边界检测] 模型 ${existingModelId} 移动到区域 ${currentZoneIndex}`);
const existingMeshes = this.mainApp.appModel?.modelDic?.Get(existingModelId);
if (existingMeshes && existingMeshes.length) {
const existingRootMesh = existingMeshes[0];
const swapZone = wallZones[currentZoneIndex];
if (swapZone) {
const offsetDistance = 0;
const swapPosition = swapZone.center.add(swapZone.normal.scale(offsetDistance));
existingRootMesh.position.copyFrom(swapPosition);
const targetDirection = swapZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
existingRootMesh.rotation.y = angle;
console.log(`[边界检测] 已将模型 ${existingModelId} 物理移动到区域 ${currentZoneIndex}`);
}
}
}
} else {
console.log(`[边界检测] 未启用替换模式或未启用占用区域处理,允许重叠`);
}
}
appDropZone["zoneModelMap"]?.set(newKey, modelId);
console.log(`[边界检测] 添加新映射: ${newKey} -> ${modelId}`);
} else {
console.log(`[边界检测] 模型 ${modelId} 仍在区域 ${currentZoneIndex},无需更新映射`);
}
}
/**
* 清理资源
*/
@ -356884,6 +357253,23 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
line.isVisible = true;
});
}
/**
* 只显示指定墙面的放置区域
* @param wallName 墙面名称
*/
showWall(wallName) {
this.hide();
this.placementZones.forEach((zone) => {
if (zone.wallName === wallName) {
zone.mesh.isVisible = true;
}
});
this.borderLines.forEach((line) => {
if (line.name.includes(`_${wallName}_`)) {
line.isVisible = true;
}
});
}
/**
* 隐藏所有放置区域
*/
@ -356920,6 +357306,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
scene;
placementWall;
appModel = null;
mainApp = null;
// 内部映射:放置区域 -> 模型ID
zoneModelMap = /* @__PURE__ */ new Map();
// 墙面 -> 当前分割数
@ -356940,6 +357327,12 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
setModelManager(appModel) {
this.appModel = appModel;
}
/**
* 设置 MainApp 引用内部使用
*/
setMainApp(mainApp) {
this.mainApp = mainApp;
}
/**
* 设置放置区域数据
* @param config 配置参数
@ -357018,6 +357411,9 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
divisions: newDivisions
};
}).filter((wall) => wall !== null);
this.dropZoneConfig.walls.forEach((wall) => {
this.wallDivisionsMap.set(wall.name, wall.divisions);
});
this.clearZones();
const zones = this.generateDropZones();
this.show();
@ -357071,6 +357467,176 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
this.zoneModelMap.set(zoneKey, modelId);
console.log(`已记录模型 ${modelId} 到区域 ${zoneKey}`);
this.checkAndAutoArrange(wallName);
}
/**
* 通知模型被删除外部调用用于更新映射和重新启用拖拽
* @param modelId 被删除的模型ID
*/
notifyModelRemoved(modelId) {
console.log(`[模型删除通知] 模型 ${modelId} 被删除`);
let removedWallName = null;
let removedZoneKey = null;
this.zoneModelMap.forEach((id, zoneKey) => {
if (id === modelId) {
removedZoneKey = zoneKey;
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
removedWallName = match[1];
}
}
});
if (removedZoneKey) {
this.zoneModelMap.delete(removedZoneKey);
console.log(`[模型删除通知] 已从映射中删除: ${removedZoneKey}`);
}
if (removedWallName) {
this.checkAndReenableDrag(removedWallName);
}
}
/**
* 检查墙面是否不满如果不满则重新启用该墙面所有模型的拖拽
* @param wallName 墙面名称
*/
checkAndReenableDrag(wallName) {
const currentDivisions = this.wallDivisionsMap.get(wallName);
if (!currentDivisions) return;
let placedCount = 0;
const placedModelIds = [];
this.zoneModelMap.forEach((modelId, zoneKey) => {
if (zoneKey.startsWith(`${wallName}[`)) {
placedCount++;
placedModelIds.push(modelId);
}
});
console.log(`[拖拽检查] 墙面 ${wallName} 当前模型数: ${placedCount}/${currentDivisions}`);
if (placedCount < currentDivisions) {
console.log(`[拖拽检查] 墙面 ${wallName} 未满,重新启用拖拽`);
placedModelIds.forEach((modelId) => {
if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === "function") {
this.mainApp.appModelDrag.setDragEnabled(modelId, true);
console.log(`[拖拽检查] ✓ 已启用模型 ${modelId} 的拖拽功能`);
}
});
}
}
/**
* 检查墙面是否已满
* @param wallName 墙面名称
* @returns 是否已满
*/
isWallFull(wallName) {
const currentDivisions = this.wallDivisionsMap.get(wallName);
if (!currentDivisions) return false;
let placedCount = 0;
this.zoneModelMap.forEach((modelId, zoneKey) => {
if (zoneKey.startsWith(`${wallName}[`)) {
placedCount++;
}
});
return placedCount >= currentDivisions;
}
/**
* 检查墙面是否已满如果满了则自动排列模型
* @param wallName 墙面名称
*/
checkAndAutoArrange(wallName) {
const currentDivisions = this.wallDivisionsMap.get(wallName);
console.log(`[自动排列检查] 墙面: ${wallName}, 分割数: ${currentDivisions}`);
if (!currentDivisions) {
console.log(`[自动排列检查] 墙面 ${wallName} 没有分割数配置,跳过`);
return;
}
let placedCount = 0;
const placedModels = [];
this.zoneModelMap.forEach((modelId, zoneKey) => {
if (zoneKey.startsWith(`${wallName}[`)) {
placedCount++;
placedModels.push(`${zoneKey} -> ${modelId}`);
}
});
console.log(`[自动排列检查] 墙面 ${wallName} 已放置模型数: ${placedCount}/${currentDivisions}`);
console.log(`[自动排列检查] 已放置的模型:`, placedModels);
if (placedCount === currentDivisions) {
console.log(`[自动排列] 墙面 ${wallName} 已满(${placedCount}/${currentDivisions}),开始执行自动排列`);
this.autoArrangeWall(wallName);
} else {
console.log(`[自动排列检查] 墙面 ${wallName} 未满,不执行自动排列`);
}
}
/**
* 自动排列墙面上的所有模型
* @param wallName 墙面名称
*/
autoArrangeWall(wallName) {
console.log(`[自动排列] 开始排列墙面: ${wallName}`);
const wallZones = this.getZonesByWall(wallName);
console.log(`[自动排列] 墙面 ${wallName} 的放置区域数量: ${wallZones.length}`);
if (!wallZones.length) {
console.log(`[自动排列] 墙面 ${wallName} 没有放置区域,退出`);
return;
}
const placedModels = [];
this.zoneModelMap.forEach((modelId, zoneKey) => {
const match = zoneKey.match(new RegExp(`^${wallName}\\[(\\d+)\\]$`));
if (match) {
const currentIndex = parseInt(match[1]);
placedModels.push({
modelId,
currentIndex
});
console.log(`[自动排列] 找到模型: ${modelId}, 当前索引: ${currentIndex}`);
}
});
console.log(`[自动排列] 收集到 ${placedModels.length} 个模型`);
placedModels.sort((a, b) => a.currentIndex - b.currentIndex);
console.log(`[自动排列] 排序后的模型顺序:`, placedModels.map((m) => `${m.modelId}(索引${m.currentIndex})`));
placedModels.forEach((model, newIndex) => {
console.log(`[自动排列] 处理模型 ${model.modelId}: 当前索引=${model.currentIndex}, 目标索引=${newIndex}`);
const targetZone = wallZones[newIndex];
if (!targetZone) {
console.warn(`[自动排列] ✗ 找不到索引 ${newIndex} 的放置区域`);
return;
}
if (this.appModel) {
const offsetDistance = 0;
const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
const targetDirection = targetZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
console.log(`[自动排列] 目标区域 ${newIndex} 的位置:`, {
center: targetZone.center,
normal: targetZone.normal,
targetPosition,
rotation: angle * 180 / Math.PI
});
const meshes = this.appModel.getCachedMeshes(model.modelId);
if (meshes && meshes.length > 0) {
const rootMesh = meshes[0];
rootMesh.position.copyFrom(targetPosition);
rootMesh.rotation.y = angle;
console.log(`[自动排列] ✓ 模型 ${model.modelId} 已移动到索引 ${newIndex} 的位置`);
} else {
console.warn(`[自动排列] ✗ 找不到模型 ${model.modelId} 的网格`);
}
if (model.currentIndex !== newIndex) {
const oldKey = `${wallName}[${model.currentIndex}]`;
const newKey = `${wallName}[${newIndex}]`;
this.zoneModelMap.delete(oldKey);
this.zoneModelMap.set(newKey, model.modelId);
console.log(`[自动排列] 更新映射: ${oldKey} -> ${newKey}`);
}
}
});
console.log(`[自动排列] 开始禁用拖拽功能`);
placedModels.forEach((model) => {
if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === "function") {
this.mainApp.appModelDrag.setDragEnabled(model.modelId, false);
console.log(`[自动排列] ✓ 已禁用模型 ${model.modelId} 的拖拽功能`);
} else {
console.warn(`[自动排列] ✗ 无法禁用模型 ${model.modelId} 的拖拽功能appModelDrag 未初始化`);
}
});
console.log(`[自动排列] 墙面 ${wallName} 自动排列完成`);
}
/**
* 获取所有放置区域
@ -357101,12 +357667,37 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
*/
show() {
this.placementWall.show();
this.setModelsPickable(false);
}
/**
* 只显示指定墙面的放置区域
* @param wallName 墙面名称
*/
showWall(wallName) {
this.placementWall.showWall(wallName);
this.setModelsPickable(false);
}
/**
* 隐藏所有放置区域
*/
hide() {
this.placementWall.hide();
this.setModelsPickable(true);
}
/**
* 设置所有已放置模型的可拾取状态
* @param pickable 是否可拾取
*/
setModelsPickable(pickable) {
if (!this.appModel) return;
this.zoneModelMap.forEach((modelId) => {
const meshes = this.appModel.getCachedMeshes(modelId);
if (meshes && meshes.length > 0) {
meshes.forEach((mesh) => {
mesh.isPickable = pickable;
});
}
});
}
/**
* 清除所有放置区域只清除网格不清除模型
@ -357498,9 +358089,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
if (this.config.position) {
this.ground.position = this.config.position;
}
if (this.config.receiveShadows) {
this.ground.receiveShadows = true;
}
this.ground.isPickable = false;
}
/** 创建材质 */
createMaterial() {
@ -357539,6 +358128,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
const gridPosition = this.config.position ? this.config.position.clone() : new Vector3(0, 0, 0);
gridPosition.y += 0.01;
this.gridGround.position = gridPosition;
this.gridGround.isPickable = false;
this.gridMaterial = new GridMaterial("gridMaterial", this.mainApp.appScene.object);
this.gridMaterial.mainColor = this.config.gridColor || new Color3(0.3, 0.3, 0.3);
this.gridMaterial.lineColor = this.config.gridColor || new Color3(0.3, 0.3, 0.3);
@ -357637,6 +358227,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
AppConfig.container = config.container;
AppConfig.modelUrlList = config.modelUrlList || [];
AppConfig.env = { ...AppConfig.env, ...config.env || {} };
AppConfig.camera = { ...AppConfig.camera, ...config.camera || {} };
AppConfig.gizmo = { ...AppConfig.gizmo, ...config.gizmo || {} };
AppConfig.outline = { ...AppConfig.outline, ...config.outline || {} };
this.appPositionGizmo.configure(AppConfig.gizmo);
@ -357663,6 +358254,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
this.appModel.initManagers();
this.appDropZone = new AppDropZone(this.appScene.object);
this.appDropZone.setModelManager(this.appModel);
this.appDropZone.setMainApp(this);
this.update();
EventBridge.sceneReady({ scene: this.appScene.object });
}
@ -357772,6 +358364,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
* @param options 材质应用选项
*/
apply: (options) => {
console.log(options);
this.mainApp.gameManager.applyMaterial(options);
}
};
@ -358114,6 +358707,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
container,
modelUrlList: params.modelUrlList || [],
env: params.env,
camera: params.camera,
gizmo: params.gizmo,
outline: params.outline
});

File diff suppressed because one or more lines are too long

View File

@ -275,33 +275,40 @@
letter-spacing: 2px;
}
/* 进度条样式 */
/* 加载遮罩样式 */
#progress-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 10px;
z-index: 1000;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
}
#progress-bar {
width: 0%;
height: 10px;
background: linear-gradient(90deg, #4CAF50, #45a049);
border-radius: 5px;
transition: width 0.1s ease;
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
}
#progress-text {
color: white;
text-align: center;
margin-top: 5px;
font-size: 14px;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
color: #fff;
font-size: 16px;
margin-top: 20px;
}
</style>
</head>
@ -312,8 +319,8 @@
<div id="canvas-container">
<canvas id="renderDom"></canvas>
<div id="progress-container" style="display: none;">
<div id="progress-bar"></div>
<div id="progress-text">0%</div>
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<!-- 生成放置区域按钮 -->
@ -466,7 +473,7 @@
</div>
<div class="category-content expanded">
<div class="option-group">
<button class="option-btn" data-option="color-3">SPFPDS10FTW</button>
<button class="option-btn" data-option="color-3">SPFADSW10FTW</button>
<button class="option-btn" data-option="color-4">SPFPDS10FTC</button>
</div>
</div>
@ -892,6 +899,7 @@
// 移除按钮事件
document.getElementById('remove-model-btn').addEventListener('click', () => {
const pickedMesh = window.getCurrentPickedMesh();
if (pickedMesh) {
const meshName = pickedMesh.name;
@ -916,9 +924,25 @@
// 监听放置区域点击事件
kernel.on('dropzone:click', async (dropzone_data) => {
// 显示进度条
const progressContainer = document.getElementById('progress-container');
if (progressContainer) {
progressContainer.style.display = 'flex';
}
getEvent(dropzone_data, sku)
});
// 监听模型加载完成事件
kernel.on('model:loaded', (data) => {
console.log('模型加载完成', data);
// 隐藏进度条
const progressContainer = document.getElementById('progress-container');
if (progressContainer) {
progressContainer.style.display = 'none';
}
});
// 存储当前选中的材质名和网格
let currentMaterialName = '';
@ -994,6 +1018,8 @@
// 暴露 kernel 到全局,方便调试
kernel.on('model:loaded', (event) => {
});
kernel.on('hotspot:click', (event) => {
console.log('热点被点击:', event);

View File

@ -44,6 +44,10 @@ export const init = async (customConfig = {}) => {
container: document.querySelector('#renderDom'),
modelUrlList: [],
env: { envPath: 'https://cdn.files.zguiy.com/zt/environment.env', intensity: 1.2, rotationY: 0.3, background: false },
camera: {
position: { x: 5, y: 2, z: 7 }, // 相机位置x-左右y-上下z-前后
target: { x: 0, y: 1, z: 0 } // 相机目标点:相机看向的位置
},
gizmo: {
position: false,
rotation: false,
@ -58,11 +62,10 @@ export const init = async (customConfig = {}) => {
}
};
// 合并用户自定义配置
const config = { ...defaultConfig, ...customConfig };
kernel.init(config);
}
//
//初始化加载模型
export const getAutoLoadModelList = async () => {
const kernel = getKernel();
@ -105,10 +108,12 @@ export const getPlacementZone = async (sku) => {
let division_include = []
// 同时包含10和13
const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku)
// 只包含10 无13 无12
const only10 = /(?=.*10)(?!.*13)(?!.*12)/.test(pergolaSku)
// 只包含10 无13 无12 无20
const only10 = /(?=.*10)(?!.*13)(?!.*12)(?!.*20)/.test(pergolaSku)
// 同时包含10和12
const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku)
// 同时包含10和20
const only10_20 = /(?=.*10)(?=.*20)/.test(pergolaSku)
// 1. 只要字符串里包含 10就返回 true
const has10 = /10/.test(sku);
@ -139,7 +144,24 @@ export const getPlacementZone = async (sku) => {
if (only10_12 && has10) {
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('前', '后')
}
}
const response = await fetch(apiConfig.getApiUrl(`/api/product-configs/by-sku/${sku}`));
const result = await response.json();
if (result.code === 200) {
@ -201,6 +223,7 @@ export const executeEvent = async (dropzone_data, result, sku) => {
// 第一次循环:处理 change_model
for (const event of result.data.events) {
if (event.event_type === 'change_model') {
const { name, file_url, model_control_type, category } = event.target_data;
// 生成唯一的模型ID
@ -208,8 +231,7 @@ export const executeEvent = async (dropzone_data, result, sku) => {
modelName = name;
kernel.dropZone.recordModelPlacement(wallName, index, name + '_' + modelId);
// 记录模型ID到SKU的映射
setSkuMapping(modelId, sku);
await kernel.model.add({
modelName: name,
@ -220,10 +242,10 @@ export const executeEvent = async (dropzone_data, result, sku) => {
enable: true,
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
step: 0.1,
// snapToZone: true, // 开关1拖拽吸附到最近的分割区域
// returnWhenOutOfBounds: false, // 开关2拖拽到区域外时返回原位置
// handleOccupiedZone: true, // 开关3处理已占用区域false=允许重叠)
// occupiedZoneAction: 'return' // 当开关3=true时的行为'return'=返回原位置,'replace'=替换
snapToZone: true, // 拖拽吸附到最近的分割区域
returnWhenOutOfBounds: true, // 拖拽到区域外时返回原位置
handleOccupiedZone: true, // 处理已占用区域false=允许重叠)
occupiedZoneAction: 'replace' // 当开关3=true时的行为'return'=返回原位置,'replace'=替换
},
transform: {
position: position,
@ -241,6 +263,8 @@ export const executeEvent = async (dropzone_data, result, sku) => {
const materialName = event.material_name;
const { color, color_map_url, normal_map_url, metallic, roughness } = event.target_data;
kernel.material.apply({
target: materialName,
modelId: modelName + '_' + modelId, // 传入 modelId只替换该模型的材质
@ -356,6 +380,8 @@ export const executeEvent2 = async (result, sku) => {
albedoColor: color,
albedoTexture: color_map_url,
normalMap: normal_map_url,
metallic: +metallic,
roughness: +roughness,
});
console.log(`百叶模型颜色已替换为 ${color}`);

View File

@ -21,8 +21,18 @@ export class AppCamera extends Monobehiver {
const canvas = AppConfig.container;
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.minZ = 0.01; // 近裁剪面
this.object.wheelPrecision = 200; // 滚轮缩放精度
@ -31,8 +41,9 @@ export class AppCamera extends Monobehiver {
// 限制垂直角范围,实现上帝视角
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);
}
/** 设置相机目标点 */

View File

@ -14,6 +14,10 @@ export const AppConfig = {
rotationY: 0,
background: false,
},
camera: {
position: { x: 0, y: 2, z: 5 },
target: { x: 0, y: 1, z: 0 },
},
gizmo: {
position: true,
rotation: false,

View File

@ -97,9 +97,6 @@ export class AppGround extends Monobehiver {
this.ground.position = this.config.position;
}
if (this.config.receiveShadows) {
this.ground.receiveShadows = true;
}
this.ground.isPickable = false;
}

View File

@ -36,6 +36,6 @@ export class AppLight extends Monobehiver {
Awake(): void {
// 主光源(模拟太阳)
const light = new DirectionalLight("sun", new Vector3(-1, -2, -1), this.mainApp.appScene.object);
light.intensity = 1.2;
light.intensity = 1;
}
}

View File

@ -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);
}
}
}

View File

@ -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 {
return this.modelDic.Get(name);

View File

@ -432,7 +432,7 @@ export class AppModelDrag extends Monobehiver {
if (originalZoneIndex !== -1) {
const originalZone = wallZones[originalZoneIndex];
if (originalZone) {
const offsetDistance = -0.05;
const offsetDistance = 0;
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance));
rootMesh.position.copyFrom(returnPosition);
@ -471,7 +471,7 @@ export class AppModelDrag extends Monobehiver {
if (originalZoneIndex !== -1) {
const originalZone = wallZones[originalZoneIndex];
if (originalZone) {
const offsetDistance = -0.05;
const offsetDistance = 0;
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance));
rootMesh.position.copyFrom(returnPosition);
@ -624,7 +624,7 @@ export class AppModelDrag extends Monobehiver {
const existingRootMesh = existingMeshes[0];
const swapZone = wallZones[currentZoneIndex];
if (swapZone) {
const offsetDistance = -0.05;
const offsetDistance = 0;
const swapPosition = swapZone.center.add(swapZone.normal.scale(offsetDistance));
existingRootMesh.position.copyFrom(swapPosition);

View File

@ -19,5 +19,6 @@ export class AppScene extends Monobehiver {
this.object = new Scene(this.mainApp.appEngin.object);
this.object.clearColor = new Color4(0, 0, 0, 0); // 透明背景
this.object.skipFrustumClipping = true; // 跳过视锥剔除优化性能
}
}

View 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;
}
}

View 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();
});
}
}

View File

@ -1,62 +1,18 @@
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 { Mesh, PBRMaterial, Texture, AbstractMesh, Color3 } from "@babylonjs/core";
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 = [];
}
/** 调试:返回当前场景中所有网格名称 */
@ -72,13 +28,7 @@ export class GameManager extends Monobehiver {
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));
}
/**
@ -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 材质配置选项
@ -761,12 +77,10 @@ export class GameManager extends Monobehiver {
}): 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) {
@ -774,12 +88,10 @@ export class GameManager extends Monobehiver {
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);
}
@ -787,11 +99,9 @@ export class GameManager extends Monobehiver {
}
});
} else {
// 没有提供 modelId全局查找保持向后兼容
// 没有提供 modelId全局查找
this.materialDic.Values().forEach(material => {
if (material.name === options.target || material.name.startsWith(`${options.target}_`)) {
console.log(material.name);
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}` : ''}`);
return;
}
console.log(options);
// 应用材质属性到目标材质
// 应用材质属性
targetMaterials.forEach(material => {
// 应用颜色
if (options.albedoColor) {
@ -810,51 +120,47 @@ export class GameManager extends Monobehiver {
material.albedoColor.copyFrom(color);
}
// 应用反照率纹理(颜色贴图)
// 应用反照率纹理
if (options.albedoTexture !== undefined) {
if (options.albedoTexture) {
material.albedoTexture = new Texture(options.albedoTexture);
material.albedoTexture = new Texture(options.albedoTexture, this.mainApp.appScene.object);
} else {
// 传入空字符串或 null 时清空贴图
material.albedoTexture = null;
}
}
// // 应用法线贴图
// if (options.normalMap !== undefined) {
// if (options.normalMap) {
// material.bumpTexture = new Texture(options.normalMap);
// } else {
// // 传入空字符串或 null 时清空贴图
// material.bumpTexture = 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);
// } else {
// // 传入空字符串或 null 时清空贴图
// material.metallicTexture = 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) {
// material.roughness = options.roughness;
// }
// 应用粗糙度
if (options.roughness !== undefined) {
if (material.roughness !== options.roughness) {
material.roughness = options.roughness;
}
}
// // 应用金属度
// if (options.metallic !== undefined) {
// material.metallic = options.metallic;
// }
// alert(typeof options.metallic + ' ' + typeof options.roughness);
// 强制刷新材质
material.markDirty();
// 应用金属度
if (options.metallic !== undefined) {
if (material.metallic !== options.metallic) {
material.metallic = options.metallic;
}
}
});
}
}
}

View File

@ -70,6 +70,7 @@ export class MainApp {
AppConfig.container = config.container;
AppConfig.modelUrlList = config.modelUrlList || [];
AppConfig.env = { ...AppConfig.env, ...(config.env || {}) };
AppConfig.camera = { ...AppConfig.camera, ...(config.camera || {}) };
AppConfig.gizmo = { ...AppConfig.gizmo, ...(config.gizmo || {}) };
AppConfig.outline = { ...AppConfig.outline, ...(config.outline || {}) };
this.appPositionGizmo.configure(AppConfig.gizmo);

View File

@ -113,6 +113,7 @@ export class KernelAdapter {
roughness?: number;
metallic?: number;
}): void => {
console.log(options);
this.mainApp.gameManager.applyMaterial(options);
},

View File

@ -23,6 +23,10 @@ type InitParams = {
rotationY?: number;
background?: boolean;
};
camera?: {
position?: { x: number; y: number; z: number };
target?: { x: number; y: number; z: number };
};
gizmo?: {
position?: boolean;
rotation?: boolean;
@ -66,6 +70,7 @@ const kernel = {
container,
modelUrlList: params.modelUrlList || [],
env: params.env,
camera: params.camera,
gizmo: params.gizmo,
outline: params.outline,
});

BIN
test.zip

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

5
设计
View File

@ -1,5 +0,0 @@
1.右侧添加选装选配的ui分类折叠棚子尺寸棚子/类型/百叶/配色 ,每个折叠下面都有四个属性 ,百叶是多选,其他的都是单选
2.页面布局也改下分画布和UI两部分
3.UI的事件预留好
一共四个面,每个面都是一个对象 [wall1:{ },wall2:{ },wall3:{ },wall4:{ }] 这种wall1:{ } 是一个对象里面包含wall1的所有属性从哪到哪的长度