1
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/public/
|
/public/
|
||||||
/dist/
|
/dist/
|
||||||
|
/assets/
|
||||||
nul
|
nul
|
||||||
169
DRAG_USAGE.md
Normal file
169
DRAG_USAGE.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# 模型拖拽功能使用说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
模型拖拽功能允许用户通过鼠标拖动 3D 模型,支持限制轴向移动,同时只能激活一个轴。
|
||||||
|
|
||||||
|
## 配置参数
|
||||||
|
|
||||||
|
在加载或替换模型时,可以添加 `drag` 参数:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
drag: {
|
||||||
|
enable: boolean, // 是否启用拖拽
|
||||||
|
axis?: string, // 允许的轴向:'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz'
|
||||||
|
step?: number, // 移动步长,默认 0.1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 1. 加载模型时配置拖拽
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 单个模型加载
|
||||||
|
await mainApp.appModel.add({
|
||||||
|
modelId: 'model1',
|
||||||
|
modelUrl: 'path/to/model.glb',
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'y', // 只允许 Y 轴移动
|
||||||
|
step: 0.1, // 每次移动 0.1 单位
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量模型加载
|
||||||
|
await mainApp.appModel.add([
|
||||||
|
{
|
||||||
|
modelId: 'model1',
|
||||||
|
modelUrl: 'path/to/model1.glb',
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'x',
|
||||||
|
step: 0.5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelId: 'model2',
|
||||||
|
modelUrl: 'path/to/model2.glb',
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'xyz', // 允许所有轴向
|
||||||
|
step: 0.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 替换模型时配置拖拽
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await mainApp.appModel.replaceModel({
|
||||||
|
modelId: 'model1',
|
||||||
|
modelUrl: 'path/to/new-model.glb',
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'xz', // 允许 X 和 Z 轴移动
|
||||||
|
step: 0.2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 动态控制拖拽
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 启用/禁用拖拽
|
||||||
|
mainApp.appModelDrag.setDragEnabled('model1', true);
|
||||||
|
mainApp.appModelDrag.setDragEnabled('model1', false);
|
||||||
|
|
||||||
|
// 切换激活的轴向
|
||||||
|
mainApp.appModelDrag.switchAxis('model1', 'x'); // 切换到 X 轴
|
||||||
|
mainApp.appModelDrag.switchAxis('model1', 'y'); // 切换到 Y 轴
|
||||||
|
mainApp.appModelDrag.switchAxis('model1', 'z'); // 切换到 Z 轴
|
||||||
|
|
||||||
|
// 获取拖拽配置
|
||||||
|
const config = mainApp.appModelDrag.getDragConfig('model1');
|
||||||
|
console.log(config);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 轴向说明
|
||||||
|
|
||||||
|
- `'x'`: 只允许沿 X 轴移动
|
||||||
|
- `'y'`: 只允许沿 Y 轴移动
|
||||||
|
- `'z'`: 只允许沿 Z 轴移动
|
||||||
|
- `'xy'`: 允许沿 X 和 Y 轴移动
|
||||||
|
- `'xz'`: 允许沿 X 和 Z 轴移动
|
||||||
|
- `'yz'`: 允许沿 Y 和 Z 轴移动
|
||||||
|
- `'xyz'`: 允许沿所有轴移动(默认)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **同时只能激活一个轴**:即使配置了多个轴(如 'xyz'),拖拽时也只会沿当前激活的轴移动
|
||||||
|
2. **默认激活轴**:拖拽开始时,会自动激活配置中的第一个可用轴
|
||||||
|
3. **步长控制**:`step` 参数控制移动的精度,值越小移动越精细
|
||||||
|
4. **模型根节点**:拖拽功能作用于模型的根节点,会移动整个模型
|
||||||
|
5. **相机控制**:拖拽模型时会自动禁用相机转动,松开鼠标后自动恢复相机控制
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MainApp } from './babylonjs/MainApp';
|
||||||
|
|
||||||
|
// 创建应用
|
||||||
|
const mainApp = new MainApp();
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
mainApp.loadAConfig({
|
||||||
|
container: document.getElementById('canvas'),
|
||||||
|
modelUrlList: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
await mainApp.Awake();
|
||||||
|
|
||||||
|
// 添加可拖拽的模型
|
||||||
|
await mainApp.appModel.add({
|
||||||
|
modelId: 'draggableModel',
|
||||||
|
modelUrl: 'models/example.glb',
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'y',
|
||||||
|
step: 0.1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听键盘事件切换轴向
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'x') {
|
||||||
|
mainApp.appModelDrag.switchAxis('draggableModel', 'x');
|
||||||
|
console.log('切换到 X 轴');
|
||||||
|
} else if (e.key === 'y') {
|
||||||
|
mainApp.appModelDrag.switchAxis('draggableModel', 'y');
|
||||||
|
console.log('切换到 Y 轴');
|
||||||
|
} else if (e.key === 'z') {
|
||||||
|
mainApp.appModelDrag.switchAxis('draggableModel', 'z');
|
||||||
|
console.log('切换到 Z 轴');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### AppModelDrag 类
|
||||||
|
|
||||||
|
#### 方法
|
||||||
|
|
||||||
|
- `configureDrag(modelId: string, config: DragConfig): void`
|
||||||
|
- 为模型配置拖拽功能
|
||||||
|
|
||||||
|
- `getDragConfig(modelId: string): DragConfig | undefined`
|
||||||
|
- 获取模型的拖拽配置
|
||||||
|
|
||||||
|
- `setDragEnabled(modelId: string, enable: boolean): void`
|
||||||
|
- 启用/禁用模型拖拽
|
||||||
|
|
||||||
|
- `switchAxis(modelId: string, axis: 'x' | 'y' | 'z'): void`
|
||||||
|
- 切换当前激活的轴向
|
||||||
|
|
||||||
|
- `dispose(): void`
|
||||||
|
- 清理资源
|
||||||
BIN
ScreenShot_2026-05-12_173410_545.png
Normal file
BIN
ScreenShot_2026-05-12_173410_545.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 481 KiB |
BIN
ScreenShot_2026-05-12_175052_657.png
Normal file
BIN
ScreenShot_2026-05-12_175052_657.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
BIN
ScreenShot_2026-05-12_175509_225.png
Normal file
BIN
ScreenShot_2026-05-12_175509_225.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 363 KiB |
168
examples/drag-example.html
Normal file
168
examples/drag-example.html
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>模型拖拽示例</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
#canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
.controls h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.controls button {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.controls button:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
.controls button.active {
|
||||||
|
background: #2196F3;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<h3>模型拖拽控制</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>切换轴向:</strong><br>
|
||||||
|
<button id="axisX" onclick="switchAxis('x')">X 轴</button>
|
||||||
|
<button id="axisY" onclick="switchAxis('y')" class="active">Y 轴</button>
|
||||||
|
<button id="axisZ" onclick="switchAxis('z')">Z 轴</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<strong>拖拽控制:</strong><br>
|
||||||
|
<button onclick="toggleDrag()">启用/禁用拖拽</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<strong>使用说明:</strong><br>
|
||||||
|
• 点击并拖动模型进行移动<br>
|
||||||
|
• 当前只能沿一个轴移动<br>
|
||||||
|
• 使用按钮切换激活的轴向<br>
|
||||||
|
• 键盘快捷键:X/Y/Z 键切换轴向
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info" style="margin-top: 10px;">
|
||||||
|
<strong>当前状态:</strong><br>
|
||||||
|
<span id="status">拖拽已启用 | 激活轴:Y</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { MainApp } from '../dist/assets/index.js';
|
||||||
|
|
||||||
|
let mainApp;
|
||||||
|
let currentAxis = 'y';
|
||||||
|
let dragEnabled = true;
|
||||||
|
|
||||||
|
// 初始化应用
|
||||||
|
async function init() {
|
||||||
|
mainApp = new MainApp();
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
mainApp.loadAConfig({
|
||||||
|
container: document.getElementById('canvas'),
|
||||||
|
modelUrlList: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化场景
|
||||||
|
await mainApp.Awake();
|
||||||
|
|
||||||
|
// 添加可拖拽的模型
|
||||||
|
await mainApp.appModel.add({
|
||||||
|
modelId: 'draggableModel',
|
||||||
|
modelUrl: 'path/to/your/model.glb', // 替换为实际模型路径
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'y',
|
||||||
|
step: 0.1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('场景初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换轴向
|
||||||
|
window.switchAxis = function(axis) {
|
||||||
|
if (!mainApp) return;
|
||||||
|
|
||||||
|
currentAxis = axis;
|
||||||
|
mainApp.appModelDrag.switchAxis('draggableModel', axis);
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
document.querySelectorAll('.controls button').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
document.getElementById('axis' + axis.toUpperCase()).classList.add('active');
|
||||||
|
|
||||||
|
// 更新状态显示
|
||||||
|
updateStatus();
|
||||||
|
console.log(`切换到 ${axis.toUpperCase()} 轴`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换拖拽启用状态
|
||||||
|
window.toggleDrag = function() {
|
||||||
|
if (!mainApp) return;
|
||||||
|
|
||||||
|
dragEnabled = !dragEnabled;
|
||||||
|
mainApp.appModelDrag.setDragEnabled('draggableModel', dragEnabled);
|
||||||
|
|
||||||
|
updateStatus();
|
||||||
|
console.log(`拖拽${dragEnabled ? '已启用' : '已禁用'}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新状态显示
|
||||||
|
function updateStatus() {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
status.textContent = `拖拽${dragEnabled ? '已启用' : '已禁用'} | 激活轴:${currentAxis.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘快捷键
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if (key === 'x' || key === 'y' || key === 'z') {
|
||||||
|
switchAxis(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动应用
|
||||||
|
init().catch(console.error);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
623
index copy.html
Normal file
623
index copy.html
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>3D Model Showcase SDK - TS</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderDom {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#config-panel {
|
||||||
|
width: 320px;
|
||||||
|
background: rgba(30, 30, 45, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#config-panel::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#config-panel::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#config-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-category {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.3s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header.active {
|
||||||
|
background: rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-arrow {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-arrow.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: grid-template-rows 0.3s ease, padding 0.3s ease;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-content.expanded {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-content>* {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn.selected {
|
||||||
|
background: rgba(76, 175, 80, 0.6);
|
||||||
|
border-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-checkbox:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-checkbox input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-checkbox label {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 12px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-bar {
|
||||||
|
width: 0%;
|
||||||
|
height: 10px;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-text {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- 画布区域 -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置面板 -->
|
||||||
|
<div id="config-panel">
|
||||||
|
<div class="config-title">选装选配</div>
|
||||||
|
|
||||||
|
<!-- 棚子尺寸 -->
|
||||||
|
<div class="config-category">
|
||||||
|
<div class="category-header" data-category="size">
|
||||||
|
<span class="category-title">棚子尺寸</span>
|
||||||
|
<span class="category-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-content">
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="option-btn" data-option="size-1">3x3米</button>
|
||||||
|
<button class="option-btn" data-option="size-2">4x4米</button>
|
||||||
|
<button class="option-btn" data-option="size-3">5x5米</button>
|
||||||
|
<button class="option-btn" data-option="size-4">6x6米</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 棚子类型 -->
|
||||||
|
<div class="config-category">
|
||||||
|
<div class="category-header" data-category="type">
|
||||||
|
<span class="category-title">棚子类型</span>
|
||||||
|
<span class="category-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-content">
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="option-btn" data-option="type-1">平顶</button>
|
||||||
|
<button class="option-btn" data-option="type-2">尖顶</button>
|
||||||
|
<button class="option-btn" data-option="type-3">弧形</button>
|
||||||
|
<button class="option-btn" data-option="type-4">异形</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 百叶 (单选) -->
|
||||||
|
<div class="config-category">
|
||||||
|
<div class="category-header" data-category="louver">
|
||||||
|
<span class="category-title">百叶</span>
|
||||||
|
<span class="category-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-content">
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="option-btn" data-option="louver-1">百叶1</button>
|
||||||
|
<button class="option-btn" data-option="louver-2">百叶2</button>
|
||||||
|
<button class="option-btn" data-option="louver-3">百叶3</button>
|
||||||
|
<button class="option-btn" data-option="louver-4">百叶4</button>
|
||||||
|
<button class="option-btn" data-option="louver-4">卷帘小</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配色 -->
|
||||||
|
<div class="config-category">
|
||||||
|
<div class="category-header" data-category="color">
|
||||||
|
<span class="category-title">配色</span>
|
||||||
|
<span class="category-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-content">
|
||||||
|
<div class="option-group">
|
||||||
|
<button class="option-btn" data-option="color-1">白色</button>
|
||||||
|
<button class="option-btn" data-option="color-2">灰色</button>
|
||||||
|
<button class="option-btn" data-option="color-3">黑色</button>
|
||||||
|
<button class="option-btn" data-option="color-4">木色</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模型信息框(用于2D转3D显示) -->
|
||||||
|
<div id="model-info-box" style="display: none;">
|
||||||
|
<div style="
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
min-width: 200px;
|
||||||
|
">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px; color: #4CAF50;">模型信息</div>
|
||||||
|
<div id="info-name" style="margin-bottom: 5px;">名称: -</div>
|
||||||
|
<div id="info-position" style="margin-bottom: 10px; font-size: 12px; color: #666;">坐标: -</div>
|
||||||
|
|
||||||
|
<!-- 颜色按钮 -->
|
||||||
|
<div id="color-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
||||||
|
<button id="color-btn-1" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">白色</button>
|
||||||
|
<button id="color-btn-2" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #000000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">黑色</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 旋转按钮 -->
|
||||||
|
<div id="rotation-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
||||||
|
<button id="rotation-btn-90" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">旋转90°</button>
|
||||||
|
<button id="rotation-btn-180" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">旋转180°</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button id="remove-model-btn" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">移除</button>
|
||||||
|
<button id="close-info-btn" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./index.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { kernel } from './src/main.ts';
|
||||||
|
|
||||||
|
// ========== UI 交互逻辑 ==========
|
||||||
|
|
||||||
|
// 折叠面板切换
|
||||||
|
document.querySelectorAll('.category-header').forEach(header => {
|
||||||
|
header.addEventListener('click', function () {
|
||||||
|
const content = this.nextElementSibling;
|
||||||
|
const arrow = this.querySelector('.category-arrow');
|
||||||
|
|
||||||
|
// 切换展开/收起状态
|
||||||
|
content.classList.toggle('expanded');
|
||||||
|
arrow.classList.toggle('expanded');
|
||||||
|
this.classList.toggle('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单选按钮逻辑
|
||||||
|
document.querySelectorAll('.option-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function () {
|
||||||
|
const optionGroup = this.parentElement;
|
||||||
|
const category = this.closest('.config-category');
|
||||||
|
const categoryName = category.querySelector('.category-header').dataset.category;
|
||||||
|
|
||||||
|
// 同一组内取消其他选中状态
|
||||||
|
optionGroup.querySelectorAll('.option-btn').forEach(b => {
|
||||||
|
b.classList.remove('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选中当前按钮
|
||||||
|
this.classList.add('selected');
|
||||||
|
|
||||||
|
// 触发自定义事件
|
||||||
|
const event = new CustomEvent('config:change', {
|
||||||
|
detail: {
|
||||||
|
category: categoryName,
|
||||||
|
value: this.dataset.option,
|
||||||
|
text: this.textContent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
|
console.log('配置变更:', {
|
||||||
|
category: categoryName,
|
||||||
|
value: this.dataset.option,
|
||||||
|
text: this.textContent
|
||||||
|
});
|
||||||
|
|
||||||
|
// 百叶模型替换逻辑
|
||||||
|
if (categoryName === "louver") {
|
||||||
|
const currentText = this.textContent;
|
||||||
|
|
||||||
|
// 根据 SKU 查询配置和事件
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:3000/api/product-configs/by-sku/${currentText}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.code === 200 && result.data) {
|
||||||
|
console.log('SKU配置数据:', result.data);
|
||||||
|
console.log('关联事件:', result.data.events);
|
||||||
|
|
||||||
|
// 使用配置数据中的模型路径(如果有)
|
||||||
|
const modelUrl = result.data.model_id
|
||||||
|
? `https://sdk.zguiy.com/resurces/model/${result.data.model_id}.glb`
|
||||||
|
: `https://sdk.zguiy.com/resurces/model/${currentText}.glb`;
|
||||||
|
|
||||||
|
console.log('替换百叶模型:', modelUrl);
|
||||||
|
await kernel.model.replace({
|
||||||
|
modelId: '卷帘小',
|
||||||
|
modelUrl: modelUrl,
|
||||||
|
modelControlType: 'color'
|
||||||
|
});
|
||||||
|
console.log(`百叶模型已替换为 ${currentText}`);
|
||||||
|
} else {
|
||||||
|
console.warn('未找到SKU配置,使用默认模型路径');
|
||||||
|
const modelUrl = `https://sdk.zguiy.com/resurces/model/${currentText}.glb`;
|
||||||
|
console.log('替换百叶模型:', modelUrl);
|
||||||
|
await kernel.model.replace({
|
||||||
|
modelId: '卷帘小',
|
||||||
|
modelUrl: modelUrl,
|
||||||
|
modelControlType: 'color'
|
||||||
|
});
|
||||||
|
console.log(`百叶模型已替换为 ${currentText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`查询SKU配置或替换模型失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 多选复选框逻辑
|
||||||
|
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', async function () {
|
||||||
|
const category = this.closest('.config-category');
|
||||||
|
const categoryName = category.querySelector('.category-header').dataset.category;
|
||||||
|
const optionGroup = this.closest('.option-group');
|
||||||
|
const checked = this.checked;
|
||||||
|
|
||||||
|
// 获取当前组所有选中的值
|
||||||
|
const selectedValues = Array.from(
|
||||||
|
optionGroup.querySelectorAll('input[type="checkbox"]:checked')
|
||||||
|
).map(cb => ({
|
||||||
|
value: cb.dataset.option,
|
||||||
|
text: cb.nextElementSibling.textContent
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 触发自定义事件
|
||||||
|
const event = new CustomEvent('config:change', {
|
||||||
|
detail: {
|
||||||
|
category: categoryName,
|
||||||
|
values: selectedValues,
|
||||||
|
checked: this.checked,
|
||||||
|
currentValue: this.dataset.option
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
|
console.log('配置变更(多选):', {
|
||||||
|
category: categoryName,
|
||||||
|
selectedValues: selectedValues,
|
||||||
|
checked: this.checked,
|
||||||
|
currentValue: this.dataset.option
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听配置变更事件(供外部使用)
|
||||||
|
document.addEventListener('config:change', function (e) {
|
||||||
|
// 这里可以根据配置变更来操作 3D 模型
|
||||||
|
// 例如:
|
||||||
|
// if (e.detail.category === 'size') {
|
||||||
|
// kernel.model.replace({ modelId: 'shed', modelUrl: `/models/shed-${e.detail.value}.glb`, modelControlType: 'rotation' });
|
||||||
|
// }
|
||||||
|
// if (e.detail.category === 'color') {
|
||||||
|
// kernel.material.apply({
|
||||||
|
// target: 'ShedMaterial',
|
||||||
|
// attribute: 'baseColor',
|
||||||
|
// value: getColorValue(e.detail.value)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 模型信息框按钮事件 ==========
|
||||||
|
|
||||||
|
// 关闭按钮事件
|
||||||
|
document.getElementById('close-info-btn').addEventListener('click', () => {
|
||||||
|
kernel.domTo3D.detach('model-info');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 白色按钮事件
|
||||||
|
document.getElementById('color-btn-1').addEventListener('click', () => {
|
||||||
|
const materialName = window.getCurrentMaterialName();
|
||||||
|
if (materialName) {
|
||||||
|
console.log('切换为白色,材质名:', materialName);
|
||||||
|
kernel.material.color(materialName, '#FFFFFF');
|
||||||
|
} else {
|
||||||
|
console.log('没有选中材质');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 黑色按钮事件
|
||||||
|
document.getElementById('color-btn-2').addEventListener('click', () => {
|
||||||
|
const materialName = window.getCurrentMaterialName();
|
||||||
|
if (materialName) {
|
||||||
|
console.log('切换为黑色,材质名:', materialName);
|
||||||
|
kernel.material.color(materialName, '#000000');
|
||||||
|
} else {
|
||||||
|
console.log('没有选中材质');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 旋转90度按钮事件
|
||||||
|
document.getElementById('rotation-btn-90').addEventListener('click', () => {
|
||||||
|
const pickedMesh = window.getCurrentPickedMesh();
|
||||||
|
if (pickedMesh) {
|
||||||
|
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
||||||
|
if (modelName) {
|
||||||
|
console.log('旋转90度,模型名:', modelName);
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: modelName,
|
||||||
|
vector3: { x: 0, y: 90, z: 0 }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('未找到模型名称');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('没有选中的网格');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 旋转180度按钮事件
|
||||||
|
document.getElementById('rotation-btn-180').addEventListener('click', () => {
|
||||||
|
const pickedMesh = window.getCurrentPickedMesh();
|
||||||
|
if (pickedMesh) {
|
||||||
|
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
||||||
|
if (modelName) {
|
||||||
|
console.log('旋转180度,模型名:', modelName);
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: modelName,
|
||||||
|
vector3: { x: 0, y: 30, z: 0 }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('未找到模型名称');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('没有选中的网格');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除按钮事件
|
||||||
|
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
||||||
|
const pickedMesh = window.getCurrentPickedMesh();
|
||||||
|
if (pickedMesh) {
|
||||||
|
const meshName = pickedMesh.name;
|
||||||
|
const success = kernel.model.remove(meshName);
|
||||||
|
if (success) {
|
||||||
|
console.log('模型已移除');
|
||||||
|
// 关闭信息框
|
||||||
|
kernel.domTo3D.detach('model-info');
|
||||||
|
} else {
|
||||||
|
console.log('移除失败:未找到该网格所属的模型');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('没有选中的网格');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
300
index.html
300
index.html
@ -67,6 +67,40 @@
|
|||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.click-info {
|
||||||
|
background: rgba(76, 175, 80, 0.2);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-info-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4caf50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-info-item {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-info-label {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-info-value {
|
||||||
|
color: #fff;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.config-category {
|
.config-category {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -227,12 +261,34 @@
|
|||||||
<div id="progress-bar"></div>
|
<div id="progress-bar"></div>
|
||||||
<div id="progress-text">0%</div>
|
<div id="progress-text">0%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成放置区域按钮 -->
|
||||||
|
<button id="dropzone-btn" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #21c7ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
z-index: 100;
|
||||||
|
">生成放置区域</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 配置面板 -->
|
<!-- 配置面板 -->
|
||||||
<div id="config-panel">
|
<div id="config-panel">
|
||||||
<div class="config-title">选装选配</div>
|
<div class="config-title">选装选配</div>
|
||||||
|
|
||||||
|
<!-- 点击信息显示区域 -->
|
||||||
|
<div id="click-info" class="click-info" style="display: none;">
|
||||||
|
<div class="click-info-title">点击信息</div>
|
||||||
|
<div id="click-info-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 棚子尺寸 -->
|
<!-- 棚子尺寸 -->
|
||||||
<div class="config-category">
|
<div class="config-category">
|
||||||
<div class="category-header" data-category="size">
|
<div class="category-header" data-category="size">
|
||||||
@ -273,7 +329,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="category-content">
|
<div class="category-content">
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="louver-1">百叶1</button>
|
<button class="option-btn" data-option="louver-1">111</button>
|
||||||
<button class="option-btn" data-option="louver-2">百叶2</button>
|
<button class="option-btn" data-option="louver-2">百叶2</button>
|
||||||
<button class="option-btn" data-option="louver-3">百叶3</button>
|
<button class="option-btn" data-option="louver-3">百叶3</button>
|
||||||
<button class="option-btn" data-option="louver-4">百叶4</button>
|
<button class="option-btn" data-option="louver-4">百叶4</button>
|
||||||
@ -290,13 +346,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="category-content">
|
<div class="category-content">
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<button class="option-btn" data-option="color-1">白色</button>
|
<button class="option-btn" data-option="color-1">222222</button>
|
||||||
<button class="option-btn" data-option="color-2">灰色</button>
|
<button class="option-btn" data-option="color-2">灰色</button>
|
||||||
<button class="option-btn" data-option="color-3">黑色</button>
|
<button class="option-btn" data-option="color-3">黑色</button>
|
||||||
<button class="option-btn" data-option="color-4">木色</button>
|
<button class="option-btn" data-option="color-4">木色</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button id="hotspot-btn">生成热点</button>
|
||||||
|
<button id="prevent-btn">生成防止区域</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -390,6 +449,10 @@
|
|||||||
|
|
||||||
<script type="module" src="./index.js"></script>
|
<script type="module" src="./index.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { kernel } from './src/main.ts';
|
import { kernel } from './src/main.ts';
|
||||||
|
|
||||||
// ========== UI 交互逻辑 ==========
|
// ========== UI 交互逻辑 ==========
|
||||||
@ -439,24 +502,178 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 百叶模型替换逻辑
|
// 百叶模型替换逻辑
|
||||||
if (categoryName === "louver") {
|
// if (categoryName === "louver") {
|
||||||
const currentText = this.textContent;
|
const currentText = this.textContent;
|
||||||
const modelUrl = `https://sdk.zguiy.com/resurces/model/${currentText}.glb`;
|
console.log(currentText);
|
||||||
console.log('替换百叶模型:', modelUrl);
|
|
||||||
try {
|
skuToFunc(currentText);
|
||||||
await kernel.model.replace({
|
// }
|
||||||
modelId: '卷帘小',
|
|
||||||
modelUrl: modelUrl,
|
|
||||||
modelControlType: 'color'
|
|
||||||
});
|
|
||||||
console.log(`百叶模型已替换为 ${currentText}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`百叶模型替换失败:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
document.querySelector('#hotspot-btn').addEventListener('click', async function () {
|
||||||
|
console.log(11111111111111);
|
||||||
|
|
||||||
|
await hotspotRequest();
|
||||||
|
})
|
||||||
|
const skuToFunc = async (currentText) => {
|
||||||
|
// 根据 SKU 查询配置和事件
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:3000/api/product-configs/by-sku/${currentText}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.code === 200 && result.data) {
|
||||||
|
console.log('SKU配置数据:', result.data);
|
||||||
|
console.log('关联事件:', result.data.events);
|
||||||
|
|
||||||
|
// 使用 for...of 循环以支持 await
|
||||||
|
for (const event of result.data.events) {
|
||||||
|
if (event.event_type === 'change_model') {
|
||||||
|
const { file_url, model_control_type, category } = event.target_data;
|
||||||
|
console.log('替换百叶模型:', event);
|
||||||
|
|
||||||
|
await kernel.model.replace({
|
||||||
|
modelId: category,
|
||||||
|
modelUrl: file_url,
|
||||||
|
modelControlType: model_control_type,
|
||||||
|
drag: {
|
||||||
|
enable: true,
|
||||||
|
axis: 'x',
|
||||||
|
step: 0.1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`百叶模型已替换为 ${currentText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.event_type === 'change_color') {
|
||||||
|
const materialName = event.material_name;
|
||||||
|
const { color, color_map_url, normal_map_url } = 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`未查询到数据`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`查询SKU配置或替换模型失败:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotspotRequest = async () => {
|
||||||
|
try {
|
||||||
|
// 从后端获取激活状态的热点列表
|
||||||
|
const response = await fetch('http://localhost:3001/api/hotspots?status=active&page=1&pageSize=100');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.code === 200 && result.data.list.length > 0) {
|
||||||
|
// 将后端数据转换为 SDK 需要的格式
|
||||||
|
const hotspots = result.data.list.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
type: 'hotspot',
|
||||||
|
name: item.name,
|
||||||
|
meshName: item.name, // 可以根据实际情况调整
|
||||||
|
icon: item.image_url,
|
||||||
|
position: [item.position_x, item.position_y, item.position_z],
|
||||||
|
radius: item.radius,
|
||||||
|
color: "#000000",
|
||||||
|
payload: {
|
||||||
|
skus: item.skus || [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 渲染热点
|
||||||
|
kernel.hotspot.render(hotspots);
|
||||||
|
console.log('热点渲染成功:', hotspots);
|
||||||
|
} else {
|
||||||
|
console.log('没有可用的热点数据');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取热点数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听热点点击事件
|
||||||
|
window.addEventListener('hotspot:click', (event) => {
|
||||||
|
console.log('热点被点击:', event.detail);
|
||||||
|
const { id, name, payload } = event.detail;
|
||||||
|
|
||||||
|
const clickInfoDiv = document.getElementById('click-info');
|
||||||
|
const clickInfoContent = document.getElementById('click-info-content');
|
||||||
|
|
||||||
|
let html = `<div class="click-info-item">
|
||||||
|
<span class="click-info-label">类型:</span>
|
||||||
|
<span class="click-info-value">热点</span>
|
||||||
|
</div>
|
||||||
|
<div class="click-info-item">
|
||||||
|
<span class="click-info-label">名称:</span>
|
||||||
|
<span class="click-info-value">${name}</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (payload && payload.skus && payload.skus.length > 0) {
|
||||||
|
html += `<div class="click-info-item">
|
||||||
|
<span class="click-info-label">关联SKU:</span>
|
||||||
|
<span class="click-info-value">${payload.skus.join(', ')}</span>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="click-info-item">
|
||||||
|
<span class="click-info-label">关联SKU:</span>
|
||||||
|
<span class="click-info-value">无</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickInfoContent.innerHTML = html;
|
||||||
|
clickInfoDiv.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听模型点击事件
|
||||||
|
window.addEventListener('model:click', (event) => {
|
||||||
|
console.log('模型被点击:', event.detail);
|
||||||
|
const { meshName, materialName, modelControlType } = event.detail;
|
||||||
|
|
||||||
|
const clickInfoDiv = document.getElementById('click-info');
|
||||||
|
const clickInfoContent = document.getElementById('click-info-content');
|
||||||
|
|
||||||
|
let html = `<div class="click-info-item">
|
||||||
|
<span class="click-info-label">类型:</span>
|
||||||
|
<span class="click-info-value">模型</span>
|
||||||
|
</div>
|
||||||
|
<div class="click-info-item">
|
||||||
|
<span class="click-info-label">网格名称:</span>
|
||||||
|
<span class="click-info-value">${meshName}</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (materialName) {
|
||||||
|
html += `<div class="click-info-item">
|
||||||
|
<span class="click-info-label">材质名称:</span>
|
||||||
|
<span class="click-info-value">${materialName}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelControlType) {
|
||||||
|
html += `<div class="click-info-item">
|
||||||
|
<span class="click-info-label">控制类型:</span>
|
||||||
|
<span class="click-info-value">${modelControlType}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickInfoContent.innerHTML = html;
|
||||||
|
clickInfoDiv.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 多选复选框逻辑
|
// 多选复选框逻辑
|
||||||
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||||
checkbox.addEventListener('change', async function () {
|
checkbox.addEventListener('change', async function () {
|
||||||
@ -521,7 +738,10 @@
|
|||||||
const materialName = window.getCurrentMaterialName();
|
const materialName = window.getCurrentMaterialName();
|
||||||
if (materialName) {
|
if (materialName) {
|
||||||
console.log('切换为白色,材质名:', materialName);
|
console.log('切换为白色,材质名:', materialName);
|
||||||
kernel.material.color(materialName, '#FFFFFF');
|
kernel.material.apply({
|
||||||
|
target: materialName,
|
||||||
|
albedoColor: '#FFFFFF',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('没有选中材质');
|
console.log('没有选中材质');
|
||||||
}
|
}
|
||||||
@ -532,7 +752,10 @@
|
|||||||
const materialName = window.getCurrentMaterialName();
|
const materialName = window.getCurrentMaterialName();
|
||||||
if (materialName) {
|
if (materialName) {
|
||||||
console.log('切换为黑色,材质名:', materialName);
|
console.log('切换为黑色,材质名:', materialName);
|
||||||
kernel.material.color(materialName, '#000000');
|
kernel.material.apply({
|
||||||
|
target: materialName,
|
||||||
|
albedoColor: '#000000',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('没有选中材质');
|
console.log('没有选中材质');
|
||||||
}
|
}
|
||||||
@ -593,6 +816,45 @@
|
|||||||
console.log('没有选中的网格');
|
console.log('没有选中的网格');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 生成放置区域按钮事件
|
||||||
|
let dropZoneVisible = false;
|
||||||
|
document.getElementById('dropzone-btn').addEventListener('click', () => {
|
||||||
|
if (!dropZoneVisible) {
|
||||||
|
// 生成放置区域(假设有一个名为"框架"的模型)
|
||||||
|
const modelName = "pergola_80s_3x3"; // 可以改成你实际的模型名称
|
||||||
|
|
||||||
|
// 先清除旧的放置区域
|
||||||
|
kernel.dropZone.clearAll();
|
||||||
|
|
||||||
|
// 生成新的放置区域
|
||||||
|
kernel.dropZone.generate({
|
||||||
|
modelName: modelName,
|
||||||
|
divisions: 4,
|
||||||
|
color: "#21c7ff",
|
||||||
|
alpha: 0.3,
|
||||||
|
thickness: 2,
|
||||||
|
offset: 0, // 改小偏移量,从 5 改为 0.5
|
||||||
|
scale: 0.9 // 缩小到80%,生成内部放置区域
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示放置区域
|
||||||
|
kernel.dropZone.showAll();
|
||||||
|
dropZoneVisible = true;
|
||||||
|
|
||||||
|
// 更新按钮文字
|
||||||
|
document.getElementById('dropzone-btn').textContent = '隐藏放置区域';
|
||||||
|
console.log('已生成并显示放置区域');
|
||||||
|
} else {
|
||||||
|
// 隐藏放置区域
|
||||||
|
kernel.dropZone.hideAll();
|
||||||
|
dropZoneVisible = false;
|
||||||
|
|
||||||
|
// 更新按钮文字
|
||||||
|
document.getElementById('dropzone-btn').textContent = '生成放置区域';
|
||||||
|
console.log('已隐藏放置区域');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
116
index.js
116
index.js
@ -15,45 +15,58 @@ const config = {
|
|||||||
gizmo: {
|
gizmo: {
|
||||||
position: true,
|
position: true,
|
||||||
rotation: true,
|
rotation: true,
|
||||||
scale: false
|
scale: true
|
||||||
},
|
},
|
||||||
outline: {
|
outline: {
|
||||||
enable: true,
|
enable: true,
|
||||||
color: "#2196F3",
|
color: "#2196F3",
|
||||||
thickness:1,
|
thickness: 1,
|
||||||
occlusionStrength:0.1,
|
occlusionStrength: 0.1,
|
||||||
occlusionThreshold:0.0002
|
occlusionThreshold: 0.0002
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
kernel.model.add({
|
|
||||||
modelId: "框架",
|
const response = await fetch('http://localhost:3001/api/models/auto-load/list')
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb",
|
const data = await response.json()
|
||||||
modelControlType: "color"
|
const models = data.data // 这就是模型列表
|
||||||
});
|
console.log(models);
|
||||||
kernel.model.add({
|
|
||||||
modelId: "卷帘大",
|
models.forEach(model => {
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘大.glb",
|
kernel.model.add({
|
||||||
modelControlType: "color"
|
modelId: model.id,
|
||||||
});
|
modelUrl: model.file_url || `http://localhost:3001${model.file_path}`,
|
||||||
kernel.model.add({
|
modelControlType: model.model_control_type,
|
||||||
modelId: "卷帘小",
|
});
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘小.glb",
|
})
|
||||||
modelControlType: "color"
|
|
||||||
});
|
// kernel.model.add({
|
||||||
kernel.model.add({
|
// modelId: "框架",
|
||||||
modelId: "小桌",
|
// modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb",
|
||||||
modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
// modelControlType: "color"
|
||||||
modelControlType: "rotation"
|
// });
|
||||||
});
|
// 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",
|
||||||
|
// modelControlType: "color"
|
||||||
|
// });
|
||||||
|
// kernel.model.add({
|
||||||
|
// modelId: "小桌",
|
||||||
|
// modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
||||||
|
// modelControlType: "rotation"
|
||||||
|
// });
|
||||||
|
|
||||||
kernel.on('model:load:progress', (data) => {
|
kernel.on('model:load:progress', (data) => {
|
||||||
console.log('模型加载事件', data);
|
console.log('模型加载事件', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
kernel.on('model:loaded', (data) => {
|
kernel.on('model:loaded', (data) => {
|
||||||
console.log('模型加载完成', data);
|
console.log('模型加载完成', data);
|
||||||
// 隐藏进度条
|
// 隐藏进度条
|
||||||
@ -70,19 +83,19 @@ kernel.on('all:ready', (data) => {
|
|||||||
attribute: 'alpha',
|
attribute: 'alpha',
|
||||||
value: 0.5,
|
value: 0.5,
|
||||||
});
|
});
|
||||||
kernel.hotspot.render([
|
// kernel.hotspot.render([
|
||||||
{
|
// {
|
||||||
id: "h1",
|
// id: "h1",
|
||||||
type: 'hotspot',
|
// type: 'hotspot',
|
||||||
name: "卷帘门",
|
// name: "卷帘门",
|
||||||
meshName: "Valve_01",
|
// meshName: "Valve_01",
|
||||||
icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true",
|
// icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true",
|
||||||
offset: [25, 25, 0],
|
// position: [25, 25, 0],
|
||||||
radius: 20,
|
// radius: 20,
|
||||||
color: "#21c7ff",
|
// color: "#21c7ff",
|
||||||
payload: { type: "valve", code: "A" },
|
// payload: { type: "valve", code: "A" },
|
||||||
},
|
// },
|
||||||
]);
|
// ]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -150,17 +163,26 @@ window.getCurrentPickedMesh = () => currentPickedMesh;
|
|||||||
// 暴露 kernel 到全局,方便调试
|
// 暴露 kernel 到全局,方便调试
|
||||||
|
|
||||||
|
|
||||||
kernel.on('hotspot:click', (data) => {
|
kernel.on('hotspot:click', (event) => {
|
||||||
console.log('热点被点击:', data);
|
console.log('热点被点击:', event);
|
||||||
const { id, name } = data
|
|
||||||
if (name === "卷帘门") {
|
|
||||||
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
|
||||||
|
|
||||||
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
const { id, name, payload } = event;
|
||||||
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
|
||||||
console.log('设置剖切:', clipHeight);
|
if (payload && payload.skus && payload.skus.length > 0) {
|
||||||
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
console.log('热点关联的SKU列表:', payload.skus);
|
||||||
|
// 这里可以根据 SKU 列表做进一步处理,比如显示产品信息
|
||||||
|
} else {
|
||||||
|
console.log('该热点没有关联SKU');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (name === "卷帘门") {
|
||||||
|
// kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||||
|
|
||||||
|
// // Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||||
|
// const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||||
|
// console.log('设置剖切:', clipHeight);
|
||||||
|
// kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
window.kernel = kernel;
|
window.kernel = kernel;
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export class AppCamera extends Monobehiver {
|
|||||||
// this.object.upperBetaLimit = Tools.ToRadians(60); // 最大垂直角(接近90度,避免万向锁)
|
// this.object.upperBetaLimit = Tools.ToRadians(60); // 最大垂直角(接近90度,避免万向锁)
|
||||||
// this.object.lowerBetaLimit = Tools.ToRadians(60); // 最小垂直角
|
// this.object.lowerBetaLimit = Tools.ToRadians(60); // 最小垂直角
|
||||||
|
|
||||||
this.object.position = new Vector3(-0, 100, 0);
|
this.object.position = new Vector3(-0, 10, 0);
|
||||||
this.setTarget(0, 2, 0);
|
this.setTarget(0, 2, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
540
src/babylonjs/AppDropZone.ts
Normal file
540
src/babylonjs/AppDropZone.ts
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
import { Scene, Mesh, MeshBuilder, StandardMaterial, Color3, Vector3, AbstractMesh, BoundingBoxGizmo } from '@babylonjs/core';
|
||||||
|
|
||||||
|
export interface DropZoneConfig {
|
||||||
|
modelName: string; // 目标模型名称
|
||||||
|
divisions: number; // 分割块数(每条边分成几块)
|
||||||
|
color?: string; // 颜色(十六进制)
|
||||||
|
alpha?: number; // 透明度
|
||||||
|
thickness?: number; // 厚度
|
||||||
|
offset?: number; // 距离模型的偏移量
|
||||||
|
scale?: number; // 整体缩放比例(0-1),用于生成内部放置区域
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppDropZone {
|
||||||
|
private scene: Scene;
|
||||||
|
private dropZones: Mesh[] = [];
|
||||||
|
private dropZoneConfigs: Map<string, any[]> = new Map(); // 存储每个模型的放置区域配置
|
||||||
|
private boundingBoxLines: Mesh[] = []; // 存储包围盒线框
|
||||||
|
|
||||||
|
constructor(scene: Scene) {
|
||||||
|
this.scene = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有放置区域
|
||||||
|
*/
|
||||||
|
getAllDropZones(): Mesh[] {
|
||||||
|
return this.dropZones;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定模型的放置区域配置
|
||||||
|
*/
|
||||||
|
getDropZoneConfigsForModel(modelName: string): any[] {
|
||||||
|
return this.dropZoneConfigs.get(modelName) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示模型的包围盒
|
||||||
|
* @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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏所有包围盒
|
||||||
|
*/
|
||||||
|
private hideBoundingBox(): void {
|
||||||
|
this.boundingBoxLines.forEach(line => line.dispose());
|
||||||
|
this.boundingBoxLines = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import { Monobehiver } from '../base/Monobehiver';
|
|||||||
import { Dictionary } from '../utils/Dictionary';
|
import { Dictionary } from '../utils/Dictionary';
|
||||||
import { AppConfig } from './AppConfig';
|
import { AppConfig } from './AppConfig';
|
||||||
import { EventBridge } from '../event/bridge';
|
import { EventBridge } from '../event/bridge';
|
||||||
|
import { DragConfig } from './AppModelDrag';
|
||||||
|
|
||||||
type LoadResult = {
|
type LoadResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -27,6 +28,7 @@ type ModelMetadata = {
|
|||||||
modelId: string;
|
modelId: string;
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
modelControlType?: ModelControlType;
|
modelControlType?: ModelControlType;
|
||||||
|
drag?: DragConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,14 +223,15 @@ export class AppModel extends Monobehiver {
|
|||||||
return await this.addSingle(
|
return await this.addSingle(
|
||||||
modelConfig.modelId,
|
modelConfig.modelId,
|
||||||
modelConfig.modelUrl,
|
modelConfig.modelUrl,
|
||||||
modelConfig.modelControlType
|
modelConfig.modelControlType,
|
||||||
|
modelConfig.drag
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加单个模型
|
* 添加单个模型
|
||||||
*/
|
*/
|
||||||
private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType): Promise<LoadResult> {
|
private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig): Promise<LoadResult> {
|
||||||
// 检查是否已存在
|
// 检查是否已存在
|
||||||
const existingMeshes = this.modelDic.Get(modelName);
|
const existingMeshes = this.modelDic.Get(modelName);
|
||||||
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
||||||
@ -250,9 +253,15 @@ export class AppModel extends Monobehiver {
|
|||||||
this.modelMetadataDic.Set(modelName, {
|
this.modelMetadataDic.Set(modelName, {
|
||||||
modelId: modelName,
|
modelId: modelName,
|
||||||
modelUrl: modelUrl,
|
modelUrl: modelUrl,
|
||||||
modelControlType: modelControlType
|
modelControlType: modelControlType,
|
||||||
|
drag: drag
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 配置拖拽功能
|
||||||
|
if (drag) {
|
||||||
|
this.mainApp.appModelDrag?.configureDrag(modelName, drag);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新 GameManager 的字典
|
// 更新 GameManager 的字典
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
this.mainApp.gameManager?.updateDictionaries();
|
||||||
|
|
||||||
@ -274,7 +283,7 @@ export class AppModel extends Monobehiver {
|
|||||||
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
|
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
|
||||||
|
|
||||||
for (let i = 0; i < models.length; i++) {
|
for (let i = 0; i < models.length; i++) {
|
||||||
const { modelId, modelUrl, modelControlType } = models[i];
|
const { modelId, modelUrl, modelControlType, drag } = models[i];
|
||||||
|
|
||||||
const result = await this.loadSingleModel(modelUrl, (event) => {
|
const result = await this.loadSingleModel(modelUrl, (event) => {
|
||||||
this.emitProgress(i, total, modelUrl, event);
|
this.emitProgress(i, total, modelUrl, event);
|
||||||
@ -288,8 +297,14 @@ export class AppModel extends Monobehiver {
|
|||||||
this.modelMetadataDic.Set(modelId, {
|
this.modelMetadataDic.Set(modelId, {
|
||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
modelUrl: modelUrl,
|
modelUrl: modelUrl,
|
||||||
modelControlType: modelControlType
|
modelControlType: modelControlType,
|
||||||
|
drag: drag
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 配置拖拽功能
|
||||||
|
if (drag) {
|
||||||
|
this.mainApp.appModelDrag?.configureDrag(modelId, drag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(result);
|
results.push(result);
|
||||||
@ -410,7 +425,8 @@ export class AppModel extends Monobehiver {
|
|||||||
return await this.addSingle(
|
return await this.addSingle(
|
||||||
modelConfig.modelId,
|
modelConfig.modelId,
|
||||||
modelConfig.modelUrl,
|
modelConfig.modelUrl,
|
||||||
modelConfig.modelControlType
|
modelConfig.modelControlType,
|
||||||
|
modelConfig.drag
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
257
src/babylonjs/AppModelDrag.ts
Normal file
257
src/babylonjs/AppModelDrag.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||||
|
import { PointerDragBehavior } from '@babylonjs/core/Behaviors/Meshes/pointerDragBehavior';
|
||||||
|
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
||||||
|
import { Scene } from '@babylonjs/core/scene';
|
||||||
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽配置接口
|
||||||
|
*/
|
||||||
|
export interface DragConfig {
|
||||||
|
enable: boolean;
|
||||||
|
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型拖拽信息
|
||||||
|
*/
|
||||||
|
interface ModelDragInfo {
|
||||||
|
config: DragConfig;
|
||||||
|
behavior: PointerDragBehavior | null;
|
||||||
|
currentAxis: 'x' | 'y' | 'z' | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型拖拽管理器 - 负责处理模型的拖拽交互
|
||||||
|
*/
|
||||||
|
export class AppModelDrag extends Monobehiver {
|
||||||
|
private modelDragMap: Map<string, ModelDragInfo>;
|
||||||
|
private scene: Scene | null;
|
||||||
|
|
||||||
|
constructor(mainApp: any) {
|
||||||
|
super(mainApp);
|
||||||
|
this.modelDragMap = new Map();
|
||||||
|
this.scene = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化拖拽管理器
|
||||||
|
*/
|
||||||
|
Awake(): void {
|
||||||
|
this.scene = this.mainApp.appScene.object;
|
||||||
|
if (!this.scene) {
|
||||||
|
console.warn('Scene not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为模型配置拖拽
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param config 拖拽配置
|
||||||
|
*/
|
||||||
|
configureDrag(modelId: string, config: DragConfig): void {
|
||||||
|
// 获取模型的根网格
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (!meshes || !meshes.length) {
|
||||||
|
console.warn(`Model not found: ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootMesh = meshes[0]; // 第一个是根节点
|
||||||
|
|
||||||
|
// 如果已存在,先移除旧的行为
|
||||||
|
const existingInfo = this.modelDragMap.get(modelId);
|
||||||
|
if (existingInfo?.behavior) {
|
||||||
|
rootMesh.removeBehavior(existingInfo.behavior);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建拖拽信息
|
||||||
|
const dragInfo: ModelDragInfo = {
|
||||||
|
config: { ...config },
|
||||||
|
behavior: null,
|
||||||
|
currentAxis: this.getFirstAvailableAxis(config.axis || 'xyz')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.enable) {
|
||||||
|
// 创建并配置拖拽行为
|
||||||
|
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
|
||||||
|
rootMesh.addBehavior(dragInfo.behavior);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modelDragMap.set(modelId, dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建拖拽行为
|
||||||
|
*/
|
||||||
|
private createDragBehavior(modelId: string, dragInfo: ModelDragInfo): PointerDragBehavior {
|
||||||
|
const axis = dragInfo.currentAxis;
|
||||||
|
let dragAxis: Vector3;
|
||||||
|
|
||||||
|
// 根据当前激活的轴创建拖拽向量
|
||||||
|
switch (axis) {
|
||||||
|
case 'x':
|
||||||
|
dragAxis = new Vector3(1, 0, 0);
|
||||||
|
break;
|
||||||
|
case 'y':
|
||||||
|
dragAxis = new Vector3(0, 1, 0);
|
||||||
|
break;
|
||||||
|
case 'z':
|
||||||
|
dragAxis = new Vector3(0, 0, 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dragAxis = new Vector3(1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建拖拽行为
|
||||||
|
const pointerDragBehavior = new PointerDragBehavior({ dragAxis: dragAxis });
|
||||||
|
|
||||||
|
// 使用世界坐标系而不是物体本地坐标系
|
||||||
|
pointerDragBehavior.useObjectOrientationForDragging = false;
|
||||||
|
|
||||||
|
// 监听拖拽开始事件
|
||||||
|
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||||
|
// 禁用相机控制
|
||||||
|
this.disableCameraControl();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听拖拽结束事件
|
||||||
|
pointerDragBehavior.onDragEndObservable.add(() => {
|
||||||
|
// 恢复相机控制
|
||||||
|
this.enableCameraControl();
|
||||||
|
});
|
||||||
|
|
||||||
|
return pointerDragBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型的拖拽配置
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
getDragConfig(modelId: string): DragConfig | undefined {
|
||||||
|
return this.modelDragMap.get(modelId)?.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/禁用模型拖拽
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param enable 是否启用
|
||||||
|
*/
|
||||||
|
setDragEnabled(modelId: string, enable: boolean): void {
|
||||||
|
const dragInfo = this.modelDragMap.get(modelId);
|
||||||
|
if (!dragInfo) return;
|
||||||
|
|
||||||
|
dragInfo.config.enable = enable;
|
||||||
|
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (!meshes || !meshes.length) return;
|
||||||
|
|
||||||
|
const rootMesh = meshes[0];
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
// 启用:创建并添加行为
|
||||||
|
if (!dragInfo.behavior) {
|
||||||
|
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
|
||||||
|
rootMesh.addBehavior(dragInfo.behavior);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 禁用:移除行为
|
||||||
|
if (dragInfo.behavior) {
|
||||||
|
rootMesh.removeBehavior(dragInfo.behavior);
|
||||||
|
dragInfo.behavior = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换激活的轴向
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param axis 要激活的轴向
|
||||||
|
*/
|
||||||
|
switchAxis(modelId: string, axis: 'x' | 'y' | 'z'): void {
|
||||||
|
const dragInfo = this.modelDragMap.get(modelId);
|
||||||
|
if (!dragInfo) return;
|
||||||
|
|
||||||
|
// 检查该轴是否在允许的轴向中
|
||||||
|
if (!this.isAxisAllowed(axis, dragInfo.config.axis || 'xyz')) {
|
||||||
|
console.warn(`Axis ${axis} is not allowed for model ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前轴
|
||||||
|
dragInfo.currentAxis = axis;
|
||||||
|
|
||||||
|
// 重新创建拖拽行为
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (!meshes || !meshes.length) return;
|
||||||
|
|
||||||
|
const rootMesh = meshes[0];
|
||||||
|
|
||||||
|
// 移除旧行为
|
||||||
|
if (dragInfo.behavior) {
|
||||||
|
rootMesh.removeBehavior(dragInfo.behavior);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新行为
|
||||||
|
if (dragInfo.config.enable) {
|
||||||
|
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
|
||||||
|
rootMesh.addBehavior(dragInfo.behavior);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置中的第一个可用轴
|
||||||
|
*/
|
||||||
|
private getFirstAvailableAxis(axisConfig: string): 'x' | 'y' | 'z' | null {
|
||||||
|
if (axisConfig.includes('x')) return 'x';
|
||||||
|
if (axisConfig.includes('y')) return 'y';
|
||||||
|
if (axisConfig.includes('z')) return 'z';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查轴是否在允许的配置中
|
||||||
|
*/
|
||||||
|
private isAxisAllowed(axis: 'x' | 'y' | 'z', axisConfig: string): boolean {
|
||||||
|
return axisConfig.includes(axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用相机控制
|
||||||
|
*/
|
||||||
|
private disableCameraControl(): void {
|
||||||
|
const camera = this.mainApp.appCamera?.object;
|
||||||
|
if (camera) {
|
||||||
|
camera.detachControl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用相机控制
|
||||||
|
*/
|
||||||
|
private enableCameraControl(): void {
|
||||||
|
const camera = this.mainApp.appCamera?.object;
|
||||||
|
const canvas = this.mainApp.appEngin?.object?.getRenderingCanvas();
|
||||||
|
if (camera && canvas) {
|
||||||
|
camera.attachControl(canvas, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
dispose(): void {
|
||||||
|
// 移除所有拖拽行为
|
||||||
|
this.modelDragMap.forEach((dragInfo, modelId) => {
|
||||||
|
if (dragInfo.behavior) {
|
||||||
|
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||||
|
if (meshes && meshes.length) {
|
||||||
|
meshes[0].removeBehavior(dragInfo.behavior);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.modelDragMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -746,42 +746,70 @@ export class GameManager extends Monobehiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用材质
|
* 应用材质属性
|
||||||
* @param target 目标对象
|
* @param options 材质配置选项
|
||||||
* @param material 材质路径
|
|
||||||
*/
|
*/
|
||||||
applyMaterial(target: string, attribute: string, value:string): void {
|
applyMaterial(options: {
|
||||||
if (attribute !== 'baseColor' || typeof value !== 'string') return;
|
target: string;
|
||||||
|
albedoColor?: string;
|
||||||
|
albedoTexture?: string;
|
||||||
|
normalMap?: string;
|
||||||
|
metallicTexture?: string;
|
||||||
|
roughness?: number;
|
||||||
|
metallic?: number;
|
||||||
|
}): void {
|
||||||
this.updateDictionaries();
|
this.updateDictionaries();
|
||||||
|
|
||||||
// 示例实现:根据目标和材质路径应用材质
|
// 1. 查找目标材质
|
||||||
// 1. 查找目标网格
|
|
||||||
const targetMaterials: PBRMaterial[] = [];
|
const targetMaterials: PBRMaterial[] = [];
|
||||||
this.materialDic.Values().forEach(material => {
|
this.materialDic.Values().forEach(material => {
|
||||||
if (material.name === target) {
|
if (material.name === options.target) {
|
||||||
targetMaterials.push(material);
|
targetMaterials.push(material);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (targetMaterials.length === 0) {
|
if (targetMaterials.length === 0) {
|
||||||
console.warn(`Target not found: ${target}`);
|
console.warn(`Material not found: ${options.target}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 处理材质路径
|
// 2. 应用材质属性到目标材质
|
||||||
// 这里可以根据材质路径加载对应的材质配置
|
|
||||||
// 例如:paint/blue 可以映射到特定的材质配置
|
|
||||||
|
|
||||||
// 3. 应用材质到目标网格
|
|
||||||
const color = Color3.FromHexString(value);
|
|
||||||
targetMaterials.forEach(material => {
|
targetMaterials.forEach(material => {
|
||||||
|
// 应用颜色
|
||||||
// 如果是 baseColor 且值是字符串(16进制颜色),转换为 Color3
|
if (options.albedoColor) {
|
||||||
|
const color = Color3.FromHexString(options.albedoColor);
|
||||||
material.albedoColor.copyFrom(color);
|
material.albedoColor.copyFrom(color);
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有纹理,颜色会作为纹理的乘法因子
|
// 应用反照率纹理(颜色贴图)
|
||||||
// 强制刷新材质
|
if (options.albedoTexture) {
|
||||||
material.markDirty();
|
material.albedoTexture = new Texture(options.albedoTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用法线贴图
|
||||||
|
if (options.normalMap) {
|
||||||
|
material.bumpTexture = new Texture(options.normalMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用金属度贴图
|
||||||
|
if (options.metallicTexture) {
|
||||||
|
material.metallicTexture = new Texture(options.metallicTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用粗糙度值
|
||||||
|
if (options.roughness !== undefined) {
|
||||||
|
material.roughness = options.roughness;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用金属度值
|
||||||
|
if (options.metallic !== undefined) {
|
||||||
|
material.metallic = options.metallic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制刷新材质
|
||||||
|
material.markDirty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`Material applied to: ${options.target}`, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import { AppHotspot } from './AppHotspot';
|
|||||||
import { AppDomTo3D } from './AppDomTo3D';
|
import { AppDomTo3D } from './AppDomTo3D';
|
||||||
import { AppSelectionOutline } from './AppSelectionOutline';
|
import { AppSelectionOutline } from './AppSelectionOutline';
|
||||||
import { AppPositionGizmo } from './AppPositionGizmo';
|
import { AppPositionGizmo } from './AppPositionGizmo';
|
||||||
|
import { AppModelDrag } from './AppModelDrag';
|
||||||
|
import { AppDropZone } from './AppDropZone';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主应用类 - 3D场景的核心控制器
|
* 主应用类 - 3D场景的核心控制器
|
||||||
@ -34,6 +36,8 @@ export class MainApp {
|
|||||||
appDomTo3D: AppDomTo3D;
|
appDomTo3D: AppDomTo3D;
|
||||||
appSelectionOutline: AppSelectionOutline;
|
appSelectionOutline: AppSelectionOutline;
|
||||||
appPositionGizmo: AppPositionGizmo;
|
appPositionGizmo: AppPositionGizmo;
|
||||||
|
appModelDrag: AppModelDrag;
|
||||||
|
appDropZone: AppDropZone;
|
||||||
gameManager: GameManager;
|
gameManager: GameManager;
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +53,7 @@ export class MainApp {
|
|||||||
this.appDomTo3D = new AppDomTo3D(this);
|
this.appDomTo3D = new AppDomTo3D(this);
|
||||||
this.appSelectionOutline = new AppSelectionOutline(this);
|
this.appSelectionOutline = new AppSelectionOutline(this);
|
||||||
this.appPositionGizmo = new AppPositionGizmo(this);
|
this.appPositionGizmo = new AppPositionGizmo(this);
|
||||||
|
this.appModelDrag = new AppModelDrag(this);
|
||||||
this.gameManager = new GameManager(this);
|
this.gameManager = new GameManager(this);
|
||||||
|
|
||||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||||
@ -85,8 +90,11 @@ export class MainApp {
|
|||||||
this.appRay.Awake();
|
this.appRay.Awake();
|
||||||
this.appSelectionOutline.init();
|
this.appSelectionOutline.init();
|
||||||
this.appPositionGizmo.Awake();
|
this.appPositionGizmo.Awake();
|
||||||
|
this.appModelDrag.Awake();
|
||||||
this.appDomTo3D.init();
|
this.appDomTo3D.init();
|
||||||
this.appModel.initManagers();
|
this.appModel.initManagers();
|
||||||
|
// 在场景创建后初始化 AppDropZone
|
||||||
|
this.appDropZone = new AppDropZone(this.appScene.object);
|
||||||
this.update();
|
this.update();
|
||||||
EventBridge.sceneReady({ scene: this.appScene.object });
|
EventBridge.sceneReady({ scene: this.appScene.object });
|
||||||
}
|
}
|
||||||
@ -106,6 +114,7 @@ export class MainApp {
|
|||||||
this.appModel?.clean();
|
this.appModel?.clean();
|
||||||
this.appEnv?.clean();
|
this.appEnv?.clean();
|
||||||
this.appPositionGizmo?.dispose();
|
this.appPositionGizmo?.dispose();
|
||||||
|
this.appModelDrag?.dispose();
|
||||||
// this.appHotspot?.clear();
|
// this.appHotspot?.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,18 @@ export const once = (eventName: string, callback: (...args: unknown[]) => void,
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const emit = (eventName: string, ...args: unknown[]): Emitter => {
|
export const emit = (eventName: string, ...args: unknown[]): Emitter => {
|
||||||
return eventBus.emit(eventName, ...args);
|
// 触发内部事件总线
|
||||||
|
const result = eventBus.emit(eventName, ...args);
|
||||||
|
|
||||||
|
// 同时触发 window 自定义事件,方便外部监听
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const customEvent = new CustomEvent(eventName, {
|
||||||
|
detail: args[0] // 传递第一个参数作为 detail
|
||||||
|
});
|
||||||
|
window.dispatchEvent(customEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAllListeners = (eventName?: string): Emitter => {
|
export const removeAllListeners = (eventName?: string): Emitter => {
|
||||||
|
|||||||
@ -66,17 +66,18 @@ export class KernelAdapter {
|
|||||||
* 应用材质
|
* 应用材质
|
||||||
* @param options 材质应用选项
|
* @param options 材质应用选项
|
||||||
*/
|
*/
|
||||||
apply: (options: { target: string; attribute: string,value:string }): void => {
|
apply: (options: {
|
||||||
this.mainApp.gameManager.applyMaterial(options.target, options.attribute,options.value);
|
target: string;
|
||||||
|
albedoColor?: string;
|
||||||
|
albedoTexture?: string;
|
||||||
|
normalMap?: string;
|
||||||
|
metallicTexture?: string;
|
||||||
|
roughness?: number;
|
||||||
|
metallic?: number;
|
||||||
|
}): void => {
|
||||||
|
this.mainApp.gameManager.applyMaterial(options);
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* 更换材质颜色
|
|
||||||
* @param materialName 材质名称
|
|
||||||
* @param hexColor 16进制颜色值,例如 "#FF0000"
|
|
||||||
*/
|
|
||||||
color: (materialName: string, hexColor: string): void => {
|
|
||||||
this.mainApp.gameManager.applyMaterial(materialName, 'baseColor', hexColor);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 卷帘门控<E997A8>?*/
|
/** 卷帘门控<E997A8>?*/
|
||||||
@ -214,6 +215,77 @@ export class KernelAdapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 放置区域管理 */
|
||||||
|
dropZone = {
|
||||||
|
/**
|
||||||
|
* 根据模型包围盒生成四周的放置区域
|
||||||
|
* @param options 配置选项
|
||||||
|
* @example
|
||||||
|
* kernel.dropZone.generate({
|
||||||
|
* modelName: "框架",
|
||||||
|
* divisions: 4,
|
||||||
|
* color: "#21c7ff",
|
||||||
|
* alpha: 0.3,
|
||||||
|
* scale: 0.8 // 缩小到80%,用于内部放置区域
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
generate: (options: {
|
||||||
|
modelName: string;
|
||||||
|
divisions: number;
|
||||||
|
color?: string;
|
||||||
|
alpha?: number;
|
||||||
|
thickness?: number;
|
||||||
|
offset?: number;
|
||||||
|
scale?: number;
|
||||||
|
}): any[] => {
|
||||||
|
return this.mainApp.appDropZone.generateDropZones(options);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 显示所有放置区域
|
||||||
|
*/
|
||||||
|
showAll: (): void => {
|
||||||
|
this.mainApp.appDropZone.showAllDropZones();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 隐藏所有放置区域
|
||||||
|
*/
|
||||||
|
hideAll: (): void => {
|
||||||
|
this.mainApp.appDropZone.hideAllDropZones();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 显示指定模型的放置区域
|
||||||
|
*/
|
||||||
|
show: (modelName: string): void => {
|
||||||
|
this.mainApp.appDropZone.showDropZonesForModel(modelName);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 隐藏指定模型的放置区域
|
||||||
|
*/
|
||||||
|
hide: (modelName: string): void => {
|
||||||
|
this.mainApp.appDropZone.hideDropZonesForModel(modelName);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 清除所有放置区域
|
||||||
|
*/
|
||||||
|
clearAll: (): void => {
|
||||||
|
this.mainApp.appDropZone.clearAllDropZones();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 清除指定模型的放置区域
|
||||||
|
*/
|
||||||
|
clear: (modelName: string): void => {
|
||||||
|
this.mainApp.appDropZone.clearDropZonesForModel(modelName);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 检查某个位置是否在放置区域内
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
debug = {
|
debug = {
|
||||||
/** 列出当前场景网格名称 */
|
/** 列出当前场景网格名称 */
|
||||||
listMeshNames: (): string[] => {
|
listMeshNames: (): string[] => {
|
||||||
|
|||||||
BIN
微信图片_20260512130148_220_3116.jpg
Normal file
BIN
微信图片_20260512130148_220_3116.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
BIN
微信图片_20260512194218_329_47.jpg
Normal file
BIN
微信图片_20260512194218_329_47.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Reference in New Issue
Block a user