diff --git a/ScreenShot_2026-05-13_110749_413.png b/ScreenShot_2026-05-13_110749_413.png new file mode 100644 index 0000000..ee96664 Binary files /dev/null and b/ScreenShot_2026-05-13_110749_413.png differ diff --git a/docs/adapter-dropzone-api.md b/docs/adapter-dropzone-api.md new file mode 100644 index 0000000..0589ef3 --- /dev/null +++ b/docs/adapter-dropzone-api.md @@ -0,0 +1,373 @@ +# 新的 Adapter API 使用指南 + +## 放置区域 API 更新 + +### 旧的调用方式(已废弃) + +```javascript +// 旧方式:基于模型包围盒 +kernel.dropZone.generate({ + modelName: "框架", + divisions: 4, + color: "#21c7ff", + alpha: 0.3 +}); + +kernel.dropZone.show("框架"); +kernel.dropZone.hide("框架"); +kernel.dropZone.clear("框架"); +``` + +### 新的调用方式 + +```javascript +// 新方式:直接定义墙面坐标 +kernel.dropZone.generate({ + walls: [ + { + name: 'front', + startPoint: [-50, 0, -50], // [x, y, z] + endPoint: [50, 0, -50], + height: 30, + divisions: 5 + }, + { + name: 'back', + startPoint: [50, 0, 50], + endPoint: [-50, 0, 50], + height: 30, + divisions: 5 + }, + { + name: 'left', + startPoint: [-50, 0, 50], + endPoint: [-50, 0, -50], + height: 30, + divisions: 4 + }, + { + name: 'right', + startPoint: [50, 0, -50], + endPoint: [50, 0, 50], + height: 30, + divisions: 4 + } + ], + color: "#21c7ff", + alpha: 0.3, + thickness: 2, + showBorder: true, + borderColor: "#ffffff" +}); + +// 显示/隐藏特定墙面 +kernel.dropZone.show('front'); +kernel.dropZone.hide('front'); + +// 显示/隐藏所有 +kernel.dropZone.showAll(); +kernel.dropZone.hideAll(); + +// 清除所有 +kernel.dropZone.clearAll(); +``` + +## 完整示例 + +### 1. 创建矩形放置区域 + +```javascript +// 定义一个 100x100 的矩形区域 +const zones = kernel.dropZone.generate({ + walls: [ + { + name: 'front', + startPoint: [-50, 0, -50], + endPoint: [50, 0, -50], + height: 30, + divisions: 10 + }, + { + name: 'back', + startPoint: [50, 0, 50], + endPoint: [-50, 0, 50], + height: 30, + divisions: 10 + }, + { + name: 'left', + startPoint: [-50, 0, 50], + endPoint: [-50, 0, -50], + height: 30, + divisions: 10 + }, + { + name: 'right', + startPoint: [50, 0, -50], + endPoint: [50, 0, 50], + height: 30, + divisions: 10 + } + ], + color: "#21c7ff", + alpha: 0.3 +}); + +console.log(`创建了 ${zones.length} 个放置区域`); +``` + +### 2. 创建L形放置区域 + +```javascript +const zones = kernel.dropZone.generate({ + walls: [ + { + name: 'wall1', + startPoint: [0, 0, 0], + endPoint: [100, 0, 0], + height: 25, + divisions: 10 + }, + { + name: 'wall2', + startPoint: [100, 0, 0], + endPoint: [100, 0, 60], + height: 25, + divisions: 6 + }, + { + name: 'wall3', + startPoint: [100, 0, 60], + endPoint: [40, 0, 60], + height: 25, + divisions: 6 + } + ], + color: "#ff6b6b", + alpha: 0.4 +}); +``` + +### 3. 创建单个展示墙 + +```javascript +const zones = kernel.dropZone.generate({ + walls: [ + { + name: 'display', + startPoint: [-30, 0, 0], + endPoint: [30, 0, 0], + height: 20, + divisions: 6 + } + ], + color: "#4ecdc4", + alpha: 0.35 +}); +``` + +### 4. 获取放置区域信息 + +```javascript +// 获取所有放置区域 +const allZones = kernel.dropZone.getAll(); +console.log('总共有', allZones.length, '个放置区域'); + +// 获取特定墙面的所有区域 +const frontZones = kernel.dropZone.getByWall('front'); +console.log('前墙有', frontZones.length, '个区域'); + +// 获取特定的某一块 +const zone = kernel.dropZone.getZone('front', 2); +if (zone) { + console.log('前墙第3块:', { + center: zone.center, + width: zone.width, + height: zone.height, + normal: zone.normal + }); +} +``` + +### 5. 检查位置是否在放置区域内 + +```javascript +const result = kernel.dropZone.checkPosition([10, 15, -50]); + +if (result.inZone) { + console.log('在放置区域内:', { + 墙面: result.wallName, + 索引: result.index, + 中心: result.center + }); +} else { + console.log('不在任何放置区域内'); +} +``` + +### 6. 显示/隐藏操作 + +```javascript +// 显示所有放置区域 +kernel.dropZone.showAll(); + +// 隐藏所有放置区域 +kernel.dropZone.hideAll(); + +// 显示特定墙面 +kernel.dropZone.show('front'); + +// 隐藏特定墙面 +kernel.dropZone.hide('back'); + +// 清除所有放置区域 +kernel.dropZone.clearAll(); +``` + +## HTML 按钮示例 + +```html + + + + + + + + + + + + + + + + + +``` + +## API 参考 + +### `kernel.dropZone.generate(options)` + +生成放置区域 + +**参数:** +- `walls` - 墙面配置数组 + - `name` - 墙面名称 + - `startPoint` - 起始点坐标 `[x, y, z]` + - `endPoint` - 结束点坐标 `[x, y, z]` + - `height` - 墙面高度 + - `divisions` - 分割数 +- `color` - 颜色(可选,默认 `#21c7ff`) +- `alpha` - 透明度(可选,默认 `0.3`) +- `thickness` - 厚度(可选,默认 `2`) +- `showBorder` - 是否显示边框(可选,默认 `false`) +- `borderColor` - 边框颜色(可选,默认 `#ffffff`) + +**返回:** 放置区域信息数组 + +### `kernel.dropZone.showAll()` + +显示所有放置区域 + +### `kernel.dropZone.hideAll()` + +隐藏所有放置区域 + +### `kernel.dropZone.show(wallName)` + +显示指定墙面的放置区域 + +**参数:** +- `wallName` - 墙面名称 + +### `kernel.dropZone.hide(wallName)` + +隐藏指定墙面的放置区域 + +**参数:** +- `wallName` - 墙面名称 + +### `kernel.dropZone.clearAll()` + +清除所有放置区域 + +### `kernel.dropZone.getAll()` + +获取所有放置区域信息 + +**返回:** 放置区域信息数组 + +### `kernel.dropZone.getByWall(wallName)` + +获取指定墙面的所有放置区域 + +**参数:** +- `wallName` - 墙面名称 + +**返回:** 放置区域信息数组 + +### `kernel.dropZone.getZone(wallName, index)` + +获取特定的放置区域 + +**参数:** +- `wallName` - 墙面名称 +- `index` - 区域索引 + +**返回:** 放置区域信息对象 + +### `kernel.dropZone.checkPosition(position)` + +检查位置是否在放置区域内 + +**参数:** +- `position` - 位置坐标 `[x, y, z]` + +**返回:** 检查结果对象 +```javascript +{ + inZone: boolean, // 是否在区域内 + zone: object, // 区域信息(如果在区域内) + wallName: string, // 墙面名称 + index: number, // 区域索引 + center: Vector3 // 中心点坐标 +} +``` + +## 迁移注意事项 + +1. **不再依赖模型名称** - 新API使用墙面名称而不是模型名称 +2. **坐标需要手动指定** - 不再自动计算包围盒,需要明确指定每个墙面的坐标 +3. **更灵活的分割** - 每个墙面可以有不同的分割数 +4. **支持任意形状** - 可以创建L形、U形等不规则形状 + +## 优势 + +- ✅ 精确控制每个墙面的位置 +- ✅ 每个墙面可以独立配置 +- ✅ 支持任意数量的墙面 +- ✅ 可以创建不规则形状 +- ✅ 不依赖模型,更灵活 diff --git a/docs/placement-area-migration.md b/docs/placement-area-migration.md new file mode 100644 index 0000000..fc45ff7 --- /dev/null +++ b/docs/placement-area-migration.md @@ -0,0 +1,407 @@ +# 放置区域 API 迁移指南 + +## 概述 + +新的放置区域系统使用**参数化墙面定义**替代了旧的**包围盒自动生成**方案,提供更灵活、更精确的控制。 + +--- + +## 主要变化 + +### 旧方案的问题 +- ❌ 依赖模型包围盒自动计算 +- ❌ 只能生成固定的矩形四周 +- ❌ 无法适应不规则形状 +- ❌ 所有墙面必须使用相同的分割数 + +### 新方案的优势 +- ✅ 直接定义每个墙面的起始和结束坐标 +- ✅ 每个墙面可以有独立的分割数 +- ✅ 支持任意数量的墙面(1个、4个、N个) +- ✅ 可以创建不规则形状(L形、U形等) +- ✅ 不依赖模型,坐标完全可控 + +--- + +## API 对比 + +### 旧 API(已废弃) + +```typescript +import { AppDropZone } from './AppDropZone'; + +const appDropZone = new AppDropZone(scene); + +// 旧的配置方式 +const zones = appDropZone.generateDropZones({ + modelName: 'myModel', // 依赖模型名称 + divisions: 5, // 所有边使用相同分割数 + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + offset: 5, // 距离模型的偏移 + scale: 1.0 // 缩放比例 +}); + +// 返回 Mesh[] +``` + +### 新 API(推荐) + +```typescript +import { AppDropZone } from './AppDropZone'; + +const appDropZone = new AppDropZone(scene); + +// 新的配置方式 +const zones = appDropZone.generateDropZones({ + walls: [ // 墙面数组 + { + name: 'front', // 墙面名称 + startPoint: new Vector3(-50, 0, -50), // 起始点 + endPoint: new Vector3(50, 0, -50), // 结束点 + height: 30, // 高度 + divisions: 5 // 这个墙面的分割数 + }, + { + name: 'back', + startPoint: new Vector3(50, 0, 50), + endPoint: new Vector3(-50, 0, 50), + height: 30, + divisions: 5 + }, + // ... 更多墙面 + ], + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + showBorder: true, // 是否显示边框 + borderColor: '#ffffff' // 边框颜色 +}); + +// 返回 PlacementZoneInfo[] +``` + +--- + +## 迁移步骤 + +### 步骤 1:确定墙面坐标 + +如果你之前使用模型包围盒,需要先获取模型的边界坐标: + +```typescript +// 获取模型的包围盒坐标 +function getModelBounds(scene: Scene, modelName: string) { + const mesh = scene.getMeshByName(modelName); + if (!mesh) return null; + + mesh.computeWorldMatrix(true); + const boundingInfo = mesh.getBoundingInfo(); + const min = boundingInfo.boundingBox.minimumWorld; + const max = boundingInfo.boundingBox.maximumWorld; + + return { + minX: min.x, + maxX: max.x, + minY: min.y, + maxY: max.y, + minZ: min.z, + maxZ: max.z + }; +} +``` + +### 步骤 2:转换为墙面配置 + +```typescript +const bounds = getModelBounds(scene, 'myModel'); +if (!bounds) return; + +const { minX, maxX, minY, maxY, minZ, maxZ } = bounds; +const height = maxY - minY; + +// 转换为新的墙面配置 +const walls = [ + { + name: 'front', + startPoint: new Vector3(minX, minY, minZ), + endPoint: new Vector3(maxX, minY, minZ), + height: height, + divisions: 5 + }, + { + name: 'back', + startPoint: new Vector3(maxX, minY, maxZ), + endPoint: new Vector3(minX, minY, maxZ), + height: height, + divisions: 5 + }, + { + name: 'left', + startPoint: new Vector3(minX, minY, maxZ), + endPoint: new Vector3(minX, minY, minZ), + height: height, + divisions: 5 + }, + { + name: 'right', + startPoint: new Vector3(maxX, minY, minZ), + endPoint: new Vector3(maxX, minY, maxZ), + height: height, + divisions: 5 + } +]; +``` + +### 步骤 3:应用偏移(如果需要) + +如果旧代码中使用了 `offset` 参数,需要手动调整坐标: + +```typescript +const offset = 5; + +// 前墙向外偏移 +walls[0].startPoint.z -= offset; +walls[0].endPoint.z -= offset; + +// 后墙向外偏移 +walls[1].startPoint.z += offset; +walls[1].endPoint.z += offset; + +// 左墙向外偏移 +walls[2].startPoint.x -= offset; +walls[2].endPoint.x -= offset; + +// 右墙向外偏移 +walls[3].startPoint.x += offset; +walls[3].endPoint.x += offset; +``` + +### 步骤 4:应用缩放(如果需要) + +如果旧代码中使用了 `scale` 参数,需要调整坐标: + +```typescript +const scale = 0.8; // 缩放到80% + +const centerX = (minX + maxX) / 2; +const centerZ = (minZ + maxZ) / 2; + +const scaledWidth = (maxX - minX) * scale; +const scaledDepth = (maxZ - minZ) * scale; + +const scaledMinX = centerX - scaledWidth / 2; +const scaledMaxX = centerX + scaledWidth / 2; +const scaledMinZ = centerZ - scaledDepth / 2; +const scaledMaxZ = centerZ + scaledDepth / 2; + +// 使用缩放后的坐标 +walls[0].startPoint = new Vector3(scaledMinX, minY, scaledMinZ); +walls[0].endPoint = new Vector3(scaledMaxX, minY, scaledMinZ); +// ... 其他墙面类似 +``` + +--- + +## 完整迁移示例 + +### 旧代码 + +```typescript +const appDropZone = new AppDropZone(scene); + +const zones = appDropZone.generateDropZones({ + modelName: 'warehouse', + divisions: 10, + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + offset: 5, + scale: 0.9 +}); +``` + +### 新代码 + +```typescript +const appDropZone = new AppDropZone(scene); + +// 1. 获取模型边界 +const bounds = getModelBounds(scene, 'warehouse'); +if (!bounds) return; + +const { minX, maxX, minY, maxY, minZ, maxZ } = bounds; + +// 2. 应用缩放 +const scale = 0.9; +const centerX = (minX + maxX) / 2; +const centerZ = (minZ + maxZ) / 2; +const width = (maxX - minX) * scale; +const depth = (maxZ - minZ) * scale; +const height = maxY - minY; + +const scaledMinX = centerX - width / 2; +const scaledMaxX = centerX + width / 2; +const scaledMinZ = centerZ - depth / 2; +const scaledMaxZ = centerZ + depth / 2; + +// 3. 应用偏移 +const offset = 5; + +// 4. 生成放置区域 +const zones = appDropZone.generateDropZones({ + walls: [ + { + name: 'front', + startPoint: new Vector3(scaledMinX, minY, scaledMinZ - offset), + endPoint: new Vector3(scaledMaxX, minY, scaledMinZ - offset), + height: height, + divisions: 10 + }, + { + name: 'back', + startPoint: new Vector3(scaledMaxX, minY, scaledMaxZ + offset), + endPoint: new Vector3(scaledMinX, minY, scaledMaxZ + offset), + height: height, + divisions: 10 + }, + { + name: 'left', + startPoint: new Vector3(scaledMinX - offset, minY, scaledMaxZ), + endPoint: new Vector3(scaledMinX - offset, minY, scaledMinZ), + height: height, + divisions: 10 + }, + { + name: 'right', + startPoint: new Vector3(scaledMaxX + offset, minY, scaledMinZ), + endPoint: new Vector3(scaledMaxX + offset, minY, scaledMaxZ), + height: height, + divisions: 10 + } + ], + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + showBorder: true +}); +``` + +--- + +## 新功能示例 + +### 1. 不同墙面使用不同分割数 + +```typescript +const zones = appDropZone.generateDropZones({ + walls: [ + { name: 'front', ..., divisions: 10 }, // 前墙10块 + { name: 'back', ..., divisions: 10 }, // 后墙10块 + { name: 'left', ..., divisions: 5 }, // 左墙5块 + { name: 'right', ..., divisions: 5 } // 右墙5块 + ], + // ... +}); +``` + +### 2. 创建L形区域 + +```typescript +const zones = appDropZone.generateDropZones({ + walls: [ + { + name: 'wall1', + startPoint: new Vector3(0, 0, 0), + endPoint: new Vector3(100, 0, 0), + height: 25, + divisions: 10 + }, + { + name: 'wall2', + startPoint: new Vector3(100, 0, 0), + endPoint: new Vector3(100, 0, 60), + height: 25, + divisions: 6 + } + ], + color: '#ff6b6b', + alpha: 0.4 +}); +``` + +### 3. 只创建单个墙面 + +```typescript +const zones = appDropZone.generateDropZones({ + walls: [ + { + name: 'display', + startPoint: new Vector3(-30, 0, 0), + endPoint: new Vector3(30, 0, 0), + height: 20, + divisions: 6 + } + ], + color: '#4ecdc4', + alpha: 0.35 +}); +``` + +--- + +## 获取放置区域信息 + +新API返回更详细的区域信息: + +```typescript +const zones = appDropZone.generateDropZones(config); + +// 每个zone包含: +zones.forEach(zone => { + console.log({ + mesh: zone.mesh, // Babylon.js Mesh对象 + wallName: zone.wallName, // 所属墙面名称 + index: zone.index, // 在该墙面上的索引 + center: zone.center, // 中心点坐标 + width: zone.width, // 宽度 + height: zone.height, // 高度 + normal: zone.normal // 法线方向 + }); +}); + +// 获取特定墙面的所有区域 +const frontZones = appDropZone.getZonesByWall('front'); + +// 获取特定的某一块 +const zone = appDropZone.getZone('front', 2); +``` + +--- + +## 常见问题 + +### Q: 我必须迁移吗? +A: 旧的API已经被完全移除,必须迁移到新API。 + +### Q: 如何快速迁移? +A: 使用上面的 `getModelBounds` 函数获取模型边界,然后转换为墙面配置。 + +### Q: 新API性能如何? +A: 新API性能更好,因为不需要计算包围盒,直接使用预定义的坐标。 + +### Q: 可以动态调整墙面吗? +A: 可以,调用 `clearAll()` 清除旧的,然后用新坐标重新生成。 + +--- + +## 总结 + +新的放置区域系统提供了: +- ✅ 更精确的控制 +- ✅ 更灵活的配置 +- ✅ 更好的性能 +- ✅ 更清晰的API + +虽然迁移需要一些工作,但新系统的灵活性和可控性值得这个投入! diff --git a/examples/drop-zone-usage.ts b/examples/drop-zone-usage.ts new file mode 100644 index 0000000..e952b6f --- /dev/null +++ b/examples/drop-zone-usage.ts @@ -0,0 +1,233 @@ +import { Scene, Vector3 } from '@babylonjs/core'; +import { AppDropZone } from '../src/babylonjs/AppDropZone'; + +/** + * 使用示例:创建矩形放置区域 + * + * 旧的API(已废弃): + * appDropZone.generateDropZones({ + * modelName: 'myModel', + * divisions: 5, + * color: '#21c7ff', + * alpha: 0.3 + * }); + * + * 新的API:直接定义墙面坐标和分割数 + */ +export function createDropZonesExample(scene: Scene) { + const appDropZone = new AppDropZone(scene); + + // 定义一个矩形区域的四个墙面 + const zones = appDropZone.generateDropZones({ + walls: [ + { + name: 'front', + startPoint: new Vector3(-50, 0, -50), // 前墙左下角 + endPoint: new Vector3(50, 0, -50), // 前墙右下角 + height: 30, + divisions: 5 // 前墙分成5块 + }, + { + name: 'back', + startPoint: new Vector3(50, 0, 50), // 后墙右下角 + endPoint: new Vector3(-50, 0, 50), // 后墙左下角 + height: 30, + divisions: 5 // 后墙分成5块 + }, + { + name: 'left', + startPoint: new Vector3(-50, 0, 50), // 左墙后下角 + endPoint: new Vector3(-50, 0, -50), // 左墙前下角 + height: 30, + divisions: 4 // 左墙分成4块 + }, + { + name: 'right', + startPoint: new Vector3(50, 0, -50), // 右墙前下角 + endPoint: new Vector3(50, 0, 50), // 右墙后下角 + height: 30, + divisions: 4 // 右墙分成4块 + } + ], + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + showBorder: true, + borderColor: '#ffffff' + }); + + console.log(`创建了 ${zones.length} 个放置区域`); + + // 获取特定区域 + const frontZones = appDropZone.getZonesByWall('front'); + console.log(`前墙有 ${frontZones.length} 个区域`); + + // 获取某一块 + const zone = appDropZone.getZone('front', 2); + if (zone) { + console.log('前墙第3块:', { + center: zone.center, + width: zone.width, + height: zone.height, + normal: zone.normal + }); + } + + return appDropZone; +} + +/** + * 使用示例:创建L形放置区域 + */ +export function createLShapeDropZones(scene: Scene) { + const appDropZone = new AppDropZone(scene); + + const zones = appDropZone.generateDropZones({ + walls: [ + { + name: 'wall1', + startPoint: new Vector3(0, 0, 0), + endPoint: new Vector3(100, 0, 0), + height: 25, + divisions: 10 + }, + { + name: 'wall2', + startPoint: new Vector3(100, 0, 0), + endPoint: new Vector3(100, 0, 60), + height: 25, + divisions: 6 + }, + { + name: 'wall3', + startPoint: new Vector3(100, 0, 60), + endPoint: new Vector3(40, 0, 60), + height: 25, + divisions: 6 + } + ], + color: '#ff6b6b', + alpha: 0.4, + showBorder: true + }); + + return appDropZone; +} + +/** + * 使用示例:创建单个展示墙 + */ +export function createDisplayWall(scene: Scene) { + const appDropZone = new AppDropZone(scene); + + const zones = appDropZone.generateDropZones({ + walls: [ + { + name: 'display', + startPoint: new Vector3(-30, 0, 0), + endPoint: new Vector3(30, 0, 0), + height: 20, + divisions: 6 + } + ], + color: '#4ecdc4', + alpha: 0.35, + thickness: 1.5 + }); + + return appDropZone; +} + +/** + * 使用示例:根据实际模型坐标创建 + * 假设你已经知道模型的边界坐标 + */ +export function createDropZonesFromModelBounds( + scene: Scene, + minX: number, + maxX: number, + minY: number, + maxY: number, + minZ: number, + maxZ: number, + divisions: number = 5 +) { + const appDropZone = new AppDropZone(scene); + + const height = maxY - minY; + + const zones = appDropZone.generateDropZones({ + walls: [ + // 前墙 + { + name: 'front', + startPoint: new Vector3(minX, minY, minZ), + endPoint: new Vector3(maxX, minY, minZ), + height: height, + divisions: divisions + }, + // 后墙 + { + name: 'back', + startPoint: new Vector3(maxX, minY, maxZ), + endPoint: new Vector3(minX, minY, maxZ), + height: height, + divisions: divisions + }, + // 左墙 + { + name: 'left', + startPoint: new Vector3(minX, minY, maxZ), + endPoint: new Vector3(minX, minY, minZ), + height: height, + divisions: divisions + }, + // 右墙 + { + name: 'right', + startPoint: new Vector3(maxX, minY, minZ), + endPoint: new Vector3(maxX, minY, maxZ), + height: height, + divisions: divisions + } + ], + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + showBorder: true + }); + + return appDropZone; +} + +/** + * 使用示例:操作放置区域 + */ +export function manipulateDropZones(appDropZone: AppDropZone) { + // 获取所有区域 + const allZones = appDropZone.getPlacementZones(); + console.log('总共有', allZones.length, '个放置区域'); + + // 遍历所有区域 + allZones.forEach((zone, index) => { + console.log(`区域 ${index}:`, { + 墙面: zone.wallName, + 索引: zone.index, + 中心: zone.center, + 尺寸: `${zone.width} x ${zone.height}`, + 法线: zone.normal + }); + }); + + // 隐藏所有区域 + appDropZone.hide(); + + // 显示所有区域 + appDropZone.show(); + + // 清除所有区域 + // appDropZone.clearAll(); + + // 销毁 + // appDropZone.dispose(); +} diff --git a/examples/placement-wall-example.ts b/examples/placement-wall-example.ts new file mode 100644 index 0000000..105a6e7 --- /dev/null +++ b/examples/placement-wall-example.ts @@ -0,0 +1,215 @@ +import { Scene, Vector3 } from '@babylonjs/core'; +import { AppPlacementWall } from '../src/babylonjs/AppPlacementWall'; + +/** + * 使用示例:创建一个矩形围栏的放置区域 + */ +export function createRectangularWalls(scene: Scene) { + const placementWall = new AppPlacementWall(scene); + + // 定义一个矩形区域的四个墙面 + const config = { + walls: [ + { + name: 'front', + startPoint: new Vector3(-50, 0, -50), // 前墙左下角 + endPoint: new Vector3(50, 0, -50), // 前墙右下角 + height: 30, + divisions: 5 // 前墙分成5块 + }, + { + name: 'back', + startPoint: new Vector3(50, 0, 50), // 后墙右下角 + endPoint: new Vector3(-50, 0, 50), // 后墙左下角 + height: 30, + divisions: 5 // 后墙分成5块 + }, + { + name: 'left', + startPoint: new Vector3(-50, 0, 50), // 左墙后下角 + endPoint: new Vector3(-50, 0, -50), // 左墙前下角 + height: 30, + divisions: 4 // 左墙分成4块 + }, + { + name: 'right', + startPoint: new Vector3(50, 0, -50), // 右墙前下角 + endPoint: new Vector3(50, 0, 50), // 右墙后下角 + height: 30, + divisions: 4 // 右墙分成4块 + } + ], + color: '#21c7ff', + alpha: 0.3, + thickness: 2, + showBorder: true, + borderColor: '#ffffff' + }; + + const zones = placementWall.generatePlacementAreas(config); + console.log(`创建了 ${zones.length} 个放置区域`); + + return placementWall; +} + +/** + * 使用示例:创建不规则形状的墙面 + */ +export function createIrregularWalls(scene: Scene) { + const placementWall = new AppPlacementWall(scene); + + // 定义一个L形区域 + const config = { + walls: [ + { + name: 'wall1', + startPoint: new Vector3(0, 0, 0), + endPoint: new Vector3(100, 0, 0), + height: 25, + divisions: 10 + }, + { + name: 'wall2', + startPoint: new Vector3(100, 0, 0), + endPoint: new Vector3(100, 0, 60), + height: 25, + divisions: 6 + }, + { + name: 'wall3', + startPoint: new Vector3(100, 0, 60), + endPoint: new Vector3(40, 0, 60), + height: 25, + divisions: 6 + } + ], + color: '#ff6b6b', + alpha: 0.4, + showBorder: true + }; + + return placementWall.generatePlacementAreas(config); +} + +/** + * 使用示例:创建单个墙面 + */ +export function createSingleWall(scene: Scene) { + const placementWall = new AppPlacementWall(scene); + + const config = { + walls: [ + { + name: 'display_wall', + startPoint: new Vector3(-30, 0, 0), + endPoint: new Vector3(30, 0, 0), + height: 20, + divisions: 6 // 分成6个展示区域 + } + ], + color: '#4ecdc4', + alpha: 0.35, + thickness: 1.5 + }; + + return placementWall.generatePlacementAreas(config); +} + +/** + * 使用示例:获取特定放置区域并操作 + */ +export function interactWithZones(placementWall: AppPlacementWall) { + // 获取所有放置区域 + const allZones = placementWall.getPlacementZones(); + console.log('总共有', allZones.length, '个放置区域'); + + // 获取特定墙面的所有区域 + const frontZones = placementWall.getZonesByWall('front'); + console.log('前墙有', frontZones.length, '个区域'); + + // 获取特定的某一块 + const zone = placementWall.getZone('front', 2); + if (zone) { + console.log('前墙第3块的中心点:', zone.center); + console.log('尺寸:', zone.width, 'x', zone.height); + console.log('法线方向:', zone.normal); + + // 可以对这个区域做特殊处理 + // 比如改变颜色、添加标签等 + } + + // 隐藏所有区域 + // placementWall.hide(); + + // 显示所有区域 + // placementWall.show(); +} + +/** + * 使用示例:根据实际场景坐标创建 + * 假设你有一个仓库模型,想在其四周创建货架放置区域 + */ +export function createWarehouseShelfAreas(scene: Scene) { + const placementWall = new AppPlacementWall(scene); + + // 假设仓库的实际坐标 + const warehouseCorners = { + frontLeft: new Vector3(-100, 0, -80), + frontRight: new Vector3(100, 0, -80), + backLeft: new Vector3(-100, 0, 80), + backRight: new Vector3(100, 0, 80) + }; + + const shelfHeight = 40; + + const config = { + walls: [ + // 前墙 - 分成20个货架位 + { + name: 'front_shelf', + startPoint: warehouseCorners.frontLeft, + endPoint: warehouseCorners.frontRight, + height: shelfHeight, + divisions: 20 + }, + // 后墙 - 分成20个货架位 + { + name: 'back_shelf', + startPoint: warehouseCorners.backRight, + endPoint: warehouseCorners.backLeft, + height: shelfHeight, + divisions: 20 + }, + // 左墙 - 分成16个货架位 + { + name: 'left_shelf', + startPoint: warehouseCorners.backLeft, + endPoint: warehouseCorners.frontLeft, + height: shelfHeight, + divisions: 16 + }, + // 右墙 - 分成16个货架位 + { + name: 'right_shelf', + startPoint: warehouseCorners.frontRight, + endPoint: warehouseCorners.backRight, + height: shelfHeight, + divisions: 16 + } + ], + color: '#ffd93d', + alpha: 0.25, + thickness: 3, + showBorder: true, + borderColor: '#ff6b35' + }; + + const zones = placementWall.generatePlacementAreas(config); + + // 为每个货架区域添加编号 + zones.forEach((zone, index) => { + console.log(`货架 ${zone.wallName}-${zone.index}: 位置 ${zone.center}`); + }); + + return placementWall; +} diff --git a/index.html b/index.html index c611898..5ec68e8 100644 --- a/index.html +++ b/index.html @@ -821,21 +821,46 @@ let dropZoneVisible = false; document.getElementById('dropzone-btn').addEventListener('click', () => { if (!dropZoneVisible) { - // 生成放置区域(假设有一个名为"框架"的模型) - const modelName = "pergola_80s_3x3"; // 可以改成你实际的模型名称 - // 先清除旧的放置区域 kernel.dropZone.clearAll(); - // 生成新的放置区域 + // 生成新的放置区域(使用新的墙面参数化API) kernel.dropZone.generate({ - modelName: modelName, - divisions: 4, + walls: [ + { + name: 'front', + startPoint: [-1.4, 0, -1.4], + endPoint: [1.4, 0, -1.4], + height: 2.3, + divisions: 5 + }, + { + name: 'back', + startPoint: [50, 0, 50], + endPoint: [-50, 0, 50], + height: 30, + divisions: 5 + }, + { + name: 'left', + startPoint: [-50, 0, 50], + endPoint: [-50, 0, -50], + height: 30, + divisions: 4 + }, + { + name: 'right', + startPoint: [50, 0, -50], + endPoint: [50, 0, 50], + height: 30, + divisions: 4 + } + ], color: "#21c7ff", alpha: 0.3, thickness: 2, - offset: 0, // 改小偏移量,从 5 改为 0.5 - scale: 0.9 // 缩小到80%,生成内部放置区域 + showBorder: true, + borderColor: "#ffffff" }); // 显示放置区域 diff --git a/src/babylonjs/AppDropZone.ts b/src/babylonjs/AppDropZone.ts index b6e54e3..b4bedf0 100644 --- a/src/babylonjs/AppDropZone.ts +++ b/src/babylonjs/AppDropZone.ts @@ -1,540 +1,84 @@ -import { Scene, Mesh, MeshBuilder, StandardMaterial, Color3, Vector3, AbstractMesh, BoundingBoxGizmo } from '@babylonjs/core'; +import { Scene, Vector3 } from '@babylonjs/core'; +import { AppPlacementWall, WallConfig, PlacementZoneInfo } from './AppPlacementWall'; +/** + * 放置区域配置 + */ export interface DropZoneConfig { - modelName: string; // 目标模型名称 - divisions: number; // 分割块数(每条边分成几块) + walls: WallConfig[]; // 墙面配置数组 color?: string; // 颜色(十六进制) alpha?: number; // 透明度 thickness?: number; // 厚度 - offset?: number; // 距离模型的偏移量 - scale?: number; // 整体缩放比例(0-1),用于生成内部放置区域 + showBorder?: boolean; // 是否显示边框 + borderColor?: string; // 边框颜色 } +/** + * 放置区域管理类(使用新的墙面参数化方案) + */ export class AppDropZone { private scene: Scene; - private dropZones: Mesh[] = []; - private dropZoneConfigs: Map = new Map(); // 存储每个模型的放置区域配置 - private boundingBoxLines: Mesh[] = []; // 存储包围盒线框 + private placementWall: AppPlacementWall; constructor(scene: Scene) { this.scene = scene; + this.placementWall = new AppPlacementWall(scene); } /** - * 根据模型包围盒生成四周的放置区域 + * 生成放置区域 * @param config 配置参数 */ - generateDropZones(config: DropZoneConfig): Mesh[] { - const { - modelName, - divisions, - color = '#21c7ff', - alpha = 0.3, - thickness = 2, - offset = 5, - scale = 1.0 - } = config; - - // 查找目标模型(支持 modelId 或 mesh name) - let targetMeshes: AbstractMesh[] | undefined; - - // 先尝试通过 modelId 查找(从 AppModel 的 modelDic) - const mainApp = (this.scene as any).mainApp; - if (mainApp?.appModel) { - targetMeshes = mainApp.appModel.getCachedMeshes(modelName); - } - - // 如果没找到,尝试通过 mesh name 查找 - if (!targetMeshes || targetMeshes.length === 0) { - const mesh = this.scene.getMeshByName(modelName); - if (mesh) { - targetMeshes = [mesh]; - } - } - - if (!targetMeshes || targetMeshes.length === 0) { - console.warn(`模型 ${modelName} 不存在`); - return []; - } - - // 计算所有网格的总包围盒(使用世界坐标) - let minX = Infinity, minY = Infinity, minZ = Infinity; - let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; - - targetMeshes.forEach(mesh => { - // 强制更新世界矩阵 - mesh.computeWorldMatrix(true); - const boundingInfo = mesh.getBoundingInfo(); - - // 获取世界空间的包围盒 - const worldMin = boundingInfo.boundingBox.minimumWorld; - const worldMax = boundingInfo.boundingBox.maximumWorld; - - minX = Math.min(minX, worldMin.x); - minY = Math.min(minY, worldMin.y); - minZ = Math.min(minZ, worldMin.z); - maxX = Math.max(maxX, worldMax.x); - maxY = Math.max(maxY, worldMax.y); - maxZ = Math.max(maxZ, worldMax.z); - }); - - console.log('包围盒坐标:', { minX, minY, minZ, maxX, maxY, maxZ }); - console.log('包围盒尺寸:', { - width: maxX - minX, - height: maxY - minY, - depth: maxZ - minZ - }); - - const width = maxX - minX; - const height = maxY - minY; - const depth = maxZ - minZ; - - // 应用缩放比例 - const centerX = (minX + maxX) / 2; - const centerY = (minY + maxY) / 2; - const centerZ = (minZ + maxZ) / 2; - - const scaledWidth = width * scale; - const scaledHeight = height * scale; - const scaledDepth = depth * scale; - - const scaledMinX = centerX - scaledWidth / 2; - const scaledMaxX = centerX + scaledWidth / 2; - const scaledMinY = centerY - scaledHeight / 2; - const scaledMaxY = centerY + scaledHeight / 2; - const scaledMinZ = centerZ - scaledDepth / 2; - const scaledMaxZ = centerZ + scaledDepth / 2; - - // 计算每块的尺寸 - const blockWidth = scaledWidth / divisions; - const blockDepth = scaledDepth / divisions; - - const zones: Mesh[] = []; - const zoneConfigs: any[] = []; - - // 创建材质 - const material = this.createDropZoneMaterial(color, alpha); - - // 前面(Z轴负方向) - for (let i = 0; i < divisions; i++) { - const x = scaledMinX + blockWidth * i + blockWidth / 2; - const z = scaledMinZ - offset; - const position = new Vector3(x, scaledMinY, z); - const zone = this.createDropZonePlane( - `dropZone_${modelName}_front_${i}`, - blockWidth, - scaledHeight, - position, - 0, - material, - thickness - ); - zones.push(zone); - zoneConfigs.push({ - position: position.clone(), - width: blockWidth, - height: scaledHeight, - rotation: 0, - side: 'front', - index: i - }); - } - - // 后面(Z轴正方向) - for (let i = 0; i < divisions; i++) { - const x = scaledMinX + blockWidth * i + blockWidth / 2; - const z = scaledMaxZ + offset; - const position = new Vector3(x, scaledMinY, z); - const zone = this.createDropZonePlane( - `dropZone_${modelName}_back_${i}`, - blockWidth, - scaledHeight, - position, - 0, - material, - thickness - ); - zones.push(zone); - zoneConfigs.push({ - position: position.clone(), - width: blockWidth, - height: scaledHeight, - rotation: 0, - side: 'back', - index: i - }); - } - - // 左侧(X轴负方向) - for (let i = 0; i < divisions; i++) { - const x = scaledMinX - offset; - const z = scaledMinZ + blockDepth * i + blockDepth / 2; - const position = new Vector3(x, scaledMinY, z); - const zone = this.createDropZonePlane( - `dropZone_${modelName}_left_${i}`, - blockDepth, - scaledHeight, - position, - Math.PI / 2, - material, - thickness - ); - zones.push(zone); - zoneConfigs.push({ - position: position.clone(), - width: blockDepth, - height: scaledHeight, - rotation: Math.PI / 2, - side: 'left', - index: i - }); - } - - // 右侧(X轴正方向) - for (let i = 0; i < divisions; i++) { - const x = scaledMaxX + offset; - const z = scaledMinZ + blockDepth * i + blockDepth / 2; - const position = new Vector3(x, scaledMinY, z); - const zone = this.createDropZonePlane( - `dropZone_${modelName}_right_${i}`, - blockDepth, - scaledHeight, - position, - Math.PI / 2, - material, - thickness - ); - zones.push(zone); - zoneConfigs.push({ - position: position.clone(), - width: blockDepth, - height: scaledHeight, - rotation: Math.PI / 2, - side: 'right', - index: i - }); - } - - // 保存配置 - this.dropZoneConfigs.set(modelName, zoneConfigs); - this.dropZones.push(...zones); - - // 显示包围盒 - this.showBoundingBox(modelName, '#ff0000'); - - // 默认隐藏 - zones.forEach(zone => zone.setEnabled(false)); - - return zones; - } - - /** - * 创建单个放置区域平面 - */ - private createDropZonePlane( - name: string, - width: number, - height: number, - position: Vector3, - rotationY: number, - material: StandardMaterial, - thickness: number - ): Mesh { - // 创建主平面 - const plane = MeshBuilder.CreatePlane(name, { - width: width, - height: height - }, this.scene); - - plane.position = position; - plane.rotation.y = rotationY; - plane.material = material; - plane.isPickable = true; // 可以被拾取,用于检测拖拽 - plane.metadata = { isDropZone: true }; // 标记为放置区域 - - // 创建边框线 - this.createBorder(plane, width, height, thickness); - - return plane; - } - - /** - * 创建边框线 - */ - private createBorder(parent: Mesh, width: number, height: number, thickness: number): void { - const halfWidth = width / 2; - const halfHeight = height / 2; - - const points = [ - new Vector3(-halfWidth, -halfHeight, -0.01), - new Vector3(halfWidth, -halfHeight, -0.01), - new Vector3(halfWidth, halfHeight, -0.01), - new Vector3(-halfWidth, halfHeight, -0.01), - new Vector3(-halfWidth, -halfHeight, -0.01) - ]; - - const border = MeshBuilder.CreateLines(`${parent.name}_border`, { - points: points - }, this.scene); - - border.color = new Color3(1, 1, 1); // 白色边框 - border.parent = parent; - } - - /** - * 创建放置区域材质 - */ - private createDropZoneMaterial(hexColor: string, alpha: number): StandardMaterial { - const material = new StandardMaterial('dropZoneMat_' + Date.now(), this.scene); - const rgb = this.hexToRgb(hexColor); - material.diffuseColor = new Color3(rgb.r, rgb.g, rgb.b); - material.alpha = alpha; - material.backFaceCulling = false; // 双面显示 - return material; - } - - /** - * 十六进制颜色转 RGB - */ - private hexToRgb(hex: string): { r: number; g: number; b: number } { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255 - } : { r: 0.13, g: 0.78, b: 1 }; - } - - /** - * 显示所有放置区域 - */ - showAllDropZones(): void { - this.dropZones.forEach(zone => zone.setEnabled(true)); - // 显示包围盒 - this.boundingBoxLines.forEach(line => line.setEnabled(true)); - } - - /** - * 隐藏所有放置区域 - */ - hideAllDropZones(): void { - this.dropZones.forEach(zone => zone.setEnabled(false)); - // 隐藏包围盒 - this.boundingBoxLines.forEach(line => line.setEnabled(false)); - } - - /** - * 显示指定模型的放置区域 - */ - showDropZonesForModel(modelName: string): void { - this.dropZones - .filter(zone => zone.name.includes(`dropZone_${modelName}_`)) - .forEach(zone => zone.setEnabled(true)); - } - - /** - * 隐藏指定模型的放置区域 - */ - hideDropZonesForModel(modelName: string): void { - this.dropZones - .filter(zone => zone.name.includes(`dropZone_${modelName}_`)) - .forEach(zone => zone.setEnabled(false)); - } - - /** - * 检查某个位置是否在放置区域内 - * @param position 要检查的位置 - * @returns 如果在放置区域内,返回该区域的配置信息,否则返回 null - */ - checkInDropZone(position: Vector3): { zone: Mesh; config: any } | null { - for (const zone of this.dropZones) { - if (!zone.isEnabled()) continue; - - // 简单的距离检测 - const distance = Vector3.Distance( - new Vector3(position.x, 0, position.z), - new Vector3(zone.position.x, 0, zone.position.z) - ); - - // 获取区域的宽度(从 scaling 或原始尺寸计算) - const zoneBounds = zone.getBoundingInfo(); - const zoneSize = zoneBounds.boundingBox.extendSize; - const maxDistance = Math.max(zoneSize.x, zoneSize.z); - - if (distance < maxDistance) { - // 找到对应的配置 - const modelName = zone.name.split('_')[1]; - const configs = this.dropZoneConfigs.get(modelName); - const configIndex = parseInt(zone.name.split('_').pop() || '0'); - const config = configs ? configs.find(c => c.index === configIndex) : null; - - return { zone, config }; - } - } - return null; - } - - /** - * 高亮某个放置区域(鼠标悬停效果) - */ - highlightDropZone(zone: Mesh): void { - const material = zone.material as StandardMaterial; - if (material) { - material.alpha = 0.6; // 增加透明度 - material.emissiveColor = new Color3(0.2, 0.2, 0.2); // 添加发光效果 - } - } - - /** - * 取消高亮 - */ - unhighlightDropZone(zone: Mesh): void { - const material = zone.material as StandardMaterial; - if (material) { - material.alpha = 0.3; // 恢复透明度 - material.emissiveColor = new Color3(0, 0, 0); // 移除发光效果 - } - } - - /** - * 清除所有放置区域 - */ - clearAllDropZones(): void { - this.dropZones.forEach(zone => { - zone.dispose(); - }); - this.dropZones = []; - this.dropZoneConfigs.clear(); - } - - /** - * 清除指定模型的放置区域 - */ - clearDropZonesForModel(modelName: string): void { - const zonesToRemove = this.dropZones.filter(zone => - zone.name.includes(`dropZone_${modelName}_`) - ); - - zonesToRemove.forEach(zone => { - zone.dispose(); - const index = this.dropZones.indexOf(zone); - if (index > -1) { - this.dropZones.splice(index, 1); - } - }); - - this.dropZoneConfigs.delete(modelName); + generateDropZones(config: DropZoneConfig): PlacementZoneInfo[] { + return this.placementWall.generatePlacementAreas(config); } /** * 获取所有放置区域 */ - getAllDropZones(): Mesh[] { - return this.dropZones; + getPlacementZones(): PlacementZoneInfo[] { + return this.placementWall.getPlacementZones(); } /** - * 获取指定模型的放置区域配置 + * 根据墙面名称获取放置区域 */ - getDropZoneConfigsForModel(modelName: string): any[] { - return this.dropZoneConfigs.get(modelName) || []; + getZonesByWall(wallName: string): PlacementZoneInfo[] { + return this.placementWall.getZonesByWall(wallName); } /** - * 显示模型的包围盒 - * @param modelName 模型名称 - * @param color 包围盒颜色 + * 根据索引获取特定放置区域 */ - private showBoundingBox(modelName: string, color: string = '#ff0000'): void { - // 查找目标模型 - let targetMeshes: AbstractMesh[] | undefined; - - const mainApp = (this.scene as any).mainApp; - if (mainApp?.appModel) { - targetMeshes = mainApp.appModel.getCachedMeshes(modelName); - } - - if (!targetMeshes || targetMeshes.length === 0) { - const mesh = this.scene.getMeshByName(modelName); - if (mesh) { - targetMeshes = [mesh]; - } - } - - if (!targetMeshes || targetMeshes.length === 0) { - console.warn(`模型 ${modelName} 不存在`); - return; - } - - // 计算总包围盒(使用世界坐标) - let minX = Infinity, minY = Infinity, minZ = Infinity; - let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; - - targetMeshes.forEach(mesh => { - // 强制更新世界矩阵 - mesh.computeWorldMatrix(true); - const boundingInfo = mesh.getBoundingInfo(); - - // 获取世界空间的包围盒 - const worldMin = boundingInfo.boundingBox.minimumWorld; - const worldMax = boundingInfo.boundingBox.maximumWorld; - - minX = Math.min(minX, worldMin.x); - minY = Math.min(minY, worldMin.y); - minZ = Math.min(minZ, worldMin.z); - maxX = Math.max(maxX, worldMax.x); - maxY = Math.max(maxY, worldMax.y); - maxZ = Math.max(maxZ, worldMax.z); - }); - - // 创建包围盒的8个顶点 - const corners = [ - new Vector3(minX, minY, minZ), - new Vector3(maxX, minY, minZ), - new Vector3(maxX, minY, maxZ), - new Vector3(minX, minY, maxZ), - new Vector3(minX, maxY, minZ), - new Vector3(maxX, maxY, minZ), - new Vector3(maxX, maxY, maxZ), - new Vector3(minX, maxY, maxZ) - ]; - - // 创建12条边 - const edges = [ - // 底面4条边 - [corners[0], corners[1]], - [corners[1], corners[2]], - [corners[2], corners[3]], - [corners[3], corners[0]], - // 顶面4条边 - [corners[4], corners[5]], - [corners[5], corners[6]], - [corners[6], corners[7]], - [corners[7], corners[4]], - // 4条竖边 - [corners[0], corners[4]], - [corners[1], corners[5]], - [corners[2], corners[6]], - [corners[3], corners[7]] - ]; - - const rgb = this.hexToRgb(color); - const lineColor = new Color3(rgb.r, rgb.g, rgb.b); - - edges.forEach((edge, index) => { - const line = MeshBuilder.CreateLines(`boundingBox_${modelName}_${index}`, { - points: edge - }, this.scene); - line.color = lineColor; - this.boundingBoxLines.push(line); - }); + getZone(wallName: string, index: number): PlacementZoneInfo | undefined { + return this.placementWall.getZone(wallName, index); } /** - * 隐藏所有包围盒 + * 显示所有放置区域 */ - private hideBoundingBox(): void { - this.boundingBoxLines.forEach(line => line.dispose()); - this.boundingBoxLines = []; + show(): void { + this.placementWall.show(); + } + + /** + * 隐藏所有放置区域 + */ + hide(): void { + this.placementWall.hide(); + } + + /** + * 清除所有放置区域 + */ + clearAll(): void { + this.placementWall.clearAll(); + } + + /** + * 销毁 + */ + dispose(): void { + this.placementWall.dispose(); } } diff --git a/src/babylonjs/AppPlacementWall.ts b/src/babylonjs/AppPlacementWall.ts new file mode 100644 index 0000000..e8ebe3a --- /dev/null +++ b/src/babylonjs/AppPlacementWall.ts @@ -0,0 +1,331 @@ +import { Scene, Mesh, MeshBuilder, StandardMaterial, Color3, Vector3 } from '@babylonjs/core'; + +/** + * 墙面配置 - 定义一个垂直面的起始和结束坐标 + */ +export interface WallConfig { + name: string; // 墙面名称(如 "front", "back", "left", "right") + startPoint: Vector3; // 起始点坐标(左下角) + endPoint: Vector3; // 结束点坐标(右下角) + height: number; // 墙面高度 + divisions: number; // 分割数(将这个面分成几块) +} + +/** + * 放置区域配置 + */ +export interface PlacementAreaConfig { + walls: WallConfig[]; // 墙面数组(可以定义1-N个面) + color?: string; // 颜色(十六进制) + alpha?: number; // 透明度 + thickness?: number; // 厚度 + showBorder?: boolean; // 是否显示边框 + borderColor?: string; // 边框颜色 +} + +/** + * 单个放置区域的信息 + */ +export interface PlacementZoneInfo { + mesh: Mesh; // 放置区域的网格 + wallName: string; // 所属墙面名称 + index: number; // 在该墙面上的索引 + center: Vector3; // 中心点坐标 + width: number; // 宽度 + height: number; // 高度 + normal: Vector3; // 法线方向 +} + +export class AppPlacementWall { + private scene: Scene; + private placementZones: PlacementZoneInfo[] = []; + private borderLines: Mesh[] = []; + + constructor(scene: Scene) { + this.scene = scene; + } + + /** + * 根据墙面配置生成放置区域 + */ + generatePlacementAreas(config: PlacementAreaConfig): PlacementZoneInfo[] { + const { + walls, + color = '#21c7ff', + alpha = 0.3, + thickness = 2, + showBorder = true, + borderColor = '#ffffff' + } = config; + + // 清除之前的放置区域 + this.clearAll(); + + const material = this.createMaterial(color, alpha); + + walls.forEach(wall => { + const zones = this.generateWallZones(wall, material, thickness); + this.placementZones.push(...zones); + + if (showBorder) { + this.createWallBorder(wall, borderColor); + } + }); + + return this.placementZones; + } + + /** + * 为单个墙面生成放置区域 + */ + private generateWallZones( + wall: WallConfig, + material: StandardMaterial, + thickness: number + ): PlacementZoneInfo[] { + const { name, startPoint, endPoint, height, divisions } = wall; + + // 计算墙面的方向向量和长度 + const direction = endPoint.subtract(startPoint); + const wallLength = direction.length(); + const normalizedDir = direction.normalize(); + + // 计算每块的宽度 + const blockWidth = wallLength / divisions; + + // 计算墙面的法线方向(垂直于墙面,指向外侧) + const normal = new Vector3(-normalizedDir.z, 0, normalizedDir.x); + + const zones: PlacementZoneInfo[] = []; + + for (let i = 0; i < divisions; i++) { + // 计算当前块的中心位置 + const offset = blockWidth * (i + 0.5); + const centerX = startPoint.x + normalizedDir.x * offset; + const centerY = startPoint.y + height / 2; + const centerZ = startPoint.z + normalizedDir.z * offset; + const center = new Vector3(centerX, centerY, centerZ); + + // 创建放置区域平面 + const plane = MeshBuilder.CreatePlane( + `placement_${name}_${i}`, + { width: blockWidth, height: height }, + this.scene + ); + + plane.position = center; + plane.material = material; + + // 让平面面向法线方向(垂直站立并朝外) + // 使用 lookAt 让平面面向外侧 + const targetPoint = center.add(normal); + plane.lookAt(targetPoint); + + // 设置厚度(通过缩放Z轴) + plane.scaling.z = thickness; + + // 启用拾取 + plane.isPickable = true; + + // 为每个块创建边框 + this.createBlockBorder(name, i, center, blockWidth, height, normalizedDir, normal); + + zones.push({ + mesh: plane, + wallName: name, + index: i, + center: center.clone(), + width: blockWidth, + height: height, + normal: normal.clone() + }); + } + + return zones; + } + + /** + * 为单个块创建边框 + */ + private createBlockBorder( + wallName: string, + index: number, + center: Vector3, + width: number, + height: number, + direction: Vector3, + normal: Vector3 + ): void { + const halfWidth = width / 2; + const halfHeight = height / 2; + + // 计算四个角点 + const bottomLeft = new Vector3( + center.x - direction.x * halfWidth, + center.y - halfHeight, + center.z - direction.z * halfWidth + ); + const bottomRight = new Vector3( + center.x + direction.x * halfWidth, + center.y - halfHeight, + center.z + direction.z * halfWidth + ); + const topLeft = new Vector3( + center.x - direction.x * halfWidth, + center.y + halfHeight, + center.z - direction.z * halfWidth + ); + const topRight = new Vector3( + center.x + direction.x * halfWidth, + center.y + halfHeight, + center.z + direction.z * halfWidth + ); + + // 创建四条边 + const edges = [ + [bottomLeft, bottomRight], // 底边 + [bottomRight, topRight], // 右边 + [topRight, topLeft], // 顶边 + [topLeft, bottomLeft] // 左边 + ]; + + edges.forEach((edge, edgeIndex) => { + const line = MeshBuilder.CreateLines( + `block_border_${wallName}_${index}_${edgeIndex}`, + { points: edge }, + this.scene + ); + line.color = new Color3(1, 1, 1); // 白色 + this.borderLines.push(line); + }); + } + + /** + * 创建墙面边框 + */ + private createWallBorder(wall: WallConfig, color: string): void { + const { name, startPoint, endPoint, height } = wall; + + // 计算四个角点 + const bottomLeft = startPoint.clone(); + const bottomRight = endPoint.clone(); + const topLeft = new Vector3(startPoint.x, startPoint.y + height, startPoint.z); + const topRight = new Vector3(endPoint.x, endPoint.y + height, endPoint.z); + + // 创建四条边 + const edges = [ + [bottomLeft, bottomRight], // 底边 + [bottomRight, topRight], // 右边 + [topRight, topLeft], // 顶边 + [topLeft, bottomLeft] // 左边 + ]; + + const rgb = this.hexToRgb(color); + const lineColor = new Color3(rgb.r, rgb.g, rgb.b); + + edges.forEach((edge, index) => { + const line = MeshBuilder.CreateLines( + `border_${name}_${index}`, + { points: edge }, + this.scene + ); + line.color = lineColor; + this.borderLines.push(line); + }); + } + + /** + * 创建材质 + */ + private createMaterial(color: string, alpha: number): StandardMaterial { + const material = new StandardMaterial('placementMaterial', this.scene); + const rgb = this.hexToRgb(color); + material.diffuseColor = new Color3(rgb.r, rgb.g, rgb.b); + material.alpha = alpha; + material.backFaceCulling = false; + return material; + } + + /** + * 十六进制颜色转RGB + */ + private hexToRgb(hex: string): { r: number; g: number; b: number } { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255 + } + : { r: 0, g: 0, b: 0 }; + } + + /** + * 获取所有放置区域 + */ + getPlacementZones(): PlacementZoneInfo[] { + return this.placementZones; + } + + /** + * 根据墙面名称获取放置区域 + */ + getZonesByWall(wallName: string): PlacementZoneInfo[] { + return this.placementZones.filter(zone => zone.wallName === wallName); + } + + /** + * 根据索引获取特定放置区域 + */ + getZone(wallName: string, index: number): PlacementZoneInfo | undefined { + return this.placementZones.find( + zone => zone.wallName === wallName && zone.index === index + ); + } + + /** + * 显示所有放置区域 + */ + show(): void { + this.placementZones.forEach(zone => { + zone.mesh.isVisible = true; + }); + this.borderLines.forEach(line => { + line.isVisible = true; + }); + } + + /** + * 隐藏所有放置区域 + */ + hide(): void { + this.placementZones.forEach(zone => { + zone.mesh.isVisible = false; + }); + this.borderLines.forEach(line => { + line.isVisible = false; + }); + } + + /** + * 清除所有放置区域 + */ + clearAll(): void { + this.placementZones.forEach(zone => { + zone.mesh.dispose(); + }); + this.placementZones = []; + + this.borderLines.forEach(line => { + line.dispose(); + }); + this.borderLines = []; + } + + /** + * 销毁 + */ + dispose(): void { + this.clearAll(); + } +} diff --git a/src/kernel/Adapter.ts b/src/kernel/Adapter.ts index 3a5ba9b..5a9dbd1 100644 --- a/src/kernel/Adapter.ts +++ b/src/kernel/Adapter.ts @@ -1,3 +1,4 @@ +import { Vector3 } from '@babylonjs/core'; import { MainApp } from '../babylonjs/MainApp'; import type { HotspotInput } from '../types/hotspot'; @@ -218,71 +219,145 @@ export class KernelAdapter { /** 放置区域管理 */ dropZone = { /** - * 根据模型包围盒生成四周的放置区域 + * 生成放置区域 * @param options 配置选项 * @example * kernel.dropZone.generate({ - * modelName: "框架", - * divisions: 4, + * walls: [ + * { + * name: 'front', + * startPoint: new Vector3(-50, 0, -50), + * endPoint: new Vector3(50, 0, -50), + * height: 30, + * divisions: 5 + * } + * ], * color: "#21c7ff", - * alpha: 0.3, - * scale: 0.8 // 缩小到80%,用于内部放置区域 + * alpha: 0.3 * }); */ generate: (options: { - modelName: string; - divisions: number; + walls: Array<{ + name: string; + startPoint: [number, number, number]; + endPoint: [number, number, number]; + height: number; + divisions: number; + }>; color?: string; alpha?: number; thickness?: number; - offset?: number; - scale?: number; + showBorder?: boolean; + borderColor?: string; }): any[] => { - return this.mainApp.appDropZone.generateDropZones(options); + + // 转换数组坐标为 Vector3 + const config = { + ...options, + walls: options.walls.map(wall => ({ + ...wall, + startPoint: new Vector3(wall.startPoint[0], wall.startPoint[1], wall.startPoint[2]), + endPoint: new Vector3(wall.endPoint[0], wall.endPoint[1], wall.endPoint[2]) + })) + }; + + return this.mainApp.appDropZone.generateDropZones(config); }, /** * 显示所有放置区域 */ showAll: (): void => { - this.mainApp.appDropZone.showAllDropZones(); + this.mainApp.appDropZone.show(); }, /** * 隐藏所有放置区域 */ hideAll: (): void => { - this.mainApp.appDropZone.hideAllDropZones(); + this.mainApp.appDropZone.hide(); }, /** - * 显示指定模型的放置区域 + * 根据墙面名称显示放置区域 */ - show: (modelName: string): void => { - this.mainApp.appDropZone.showDropZonesForModel(modelName); + show: (wallName: string): void => { + const zones = this.mainApp.appDropZone.getZonesByWall(wallName); + zones.forEach(zone => { + zone.mesh.isVisible = true; + }); }, /** - * 隐藏指定模型的放置区域 + * 根据墙面名称隐藏放置区域 */ - hide: (modelName: string): void => { - this.mainApp.appDropZone.hideDropZonesForModel(modelName); + hide: (wallName: string): void => { + const zones = this.mainApp.appDropZone.getZonesByWall(wallName); + zones.forEach(zone => { + zone.mesh.isVisible = false; + }); }, /** * 清除所有放置区域 */ clearAll: (): void => { - this.mainApp.appDropZone.clearAllDropZones(); + this.mainApp.appDropZone.clearAll(); }, /** - * 清除指定模型的放置区域 + * 获取所有放置区域 */ - clear: (modelName: string): void => { - this.mainApp.appDropZone.clearDropZonesForModel(modelName); + getAll: (): any[] => { + return this.mainApp.appDropZone.getPlacementZones(); + }, + /** + * 根据墙面名称获取放置区域 + */ + getByWall: (wallName: string): any[] => { + return this.mainApp.appDropZone.getZonesByWall(wallName); + }, + /** + * 获取特定的放置区域 + */ + getZone: (wallName: string, index: number): any => { + return this.mainApp.appDropZone.getZone(wallName, index); }, /** * 检查某个位置是否在放置区域内 */ checkPosition: (position: [number, number, number]): any => { - const { Vector3 } = require('@babylonjs/core'); + const pos = new Vector3(position[0], position[1], position[2]); - return this.mainApp.appDropZone.checkInDropZone(pos); + + const zones = this.mainApp.appDropZone.getPlacementZones(); + + for (const zone of zones) { + const halfWidth = zone.width / 2; + const halfHeight = zone.height / 2; + + const localPos = pos.subtract(zone.center); + const distance = Math.abs( + localPos.x * zone.normal.x + + localPos.z * zone.normal.z + ); + + // 检查是否在平面附近 + if (distance < 5) { + const alongWidth = Math.abs(localPos.x * (1 - Math.abs(zone.normal.x)) + + localPos.z * (1 - Math.abs(zone.normal.z))); + const alongHeight = Math.abs(localPos.y); + + if (alongWidth <= halfWidth && alongHeight <= halfHeight) { + return { + inZone: true, + zone: zone, + wallName: zone.wallName, + index: zone.index, + center: zone.center + }; + } + } + } + + return { + inZone: false, + zone: null + }; } }; diff --git a/test-rotation.js b/test-rotation.js new file mode 100644 index 0000000..c7007a7 --- /dev/null +++ b/test-rotation.js @@ -0,0 +1,23 @@ +// 测试不同的旋转方案 + +// 方案1:只旋转Y轴(当前方案) +plane.rotation.y = angle; + +// 方案2:先X轴90度,再Y轴 +plane.rotation.x = Math.PI / 2; +plane.rotation.y = angle; + +// 方案3:使用lookAt +plane.lookAt(center.add(normal)); + +// 方案4:使用四元数旋转 +const forward = new Vector3(0, 0, -1); // 平面默认法线 +const targetNormal = normal.normalize(); +const quaternion = Quaternion.FromUnitVectorsToRef(forward, targetNormal, new Quaternion()); +plane.rotationQuaternion = quaternion; + +// 方案5:手动设置旋转(根据墙面方向) +// 前墙 (Z负方向): rotation.y = 0 +// 后墙 (Z正方向): rotation.y = Math.PI +// 左墙 (X负方向): rotation.y = Math.PI / 2 +// 右墙 (X正方向): rotation.y = -Math.PI / 2 diff --git a/wechat_2026-05-13_110515_365.png b/wechat_2026-05-13_110515_365.png new file mode 100644 index 0000000..d32e3ce Binary files /dev/null and b/wechat_2026-05-13_110515_365.png differ diff --git a/设计 b/设计 index f8fb782..25fc8e0 100644 --- a/设计 +++ b/设计 @@ -1,3 +1,5 @@ 1.右侧添加选装选配的ui,分类折叠,棚子尺寸棚子/类型/百叶/配色 ,每个折叠下面都有四个属性 ,百叶是多选,其他的都是单选 2.页面布局也改下,分画布和UI两部分 -3.UI的事件预留好 \ No newline at end of file +3.UI的事件预留好 + +一共四个面,每个面都是一个对象 [wall1:{ },wall2:{ },wall3:{ },wall4:{ }] 这种,wall1:{ } 是一个对象,里面包含wall1的所有属性,从哪到哪的长度, \ No newline at end of file