1
This commit is contained in:
216
API_USAGE_EXAMPLE.md
Normal file
216
API_USAGE_EXAMPLE.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# 模型管理 API 使用示例
|
||||||
|
|
||||||
|
## API 说明
|
||||||
|
|
||||||
|
### 1. 添加模型
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 添加带有 rotation 控制类型的模型
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘大.glb",
|
||||||
|
modelControlType: "rotation"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加带有 color 控制类型的模型
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "小桌",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
||||||
|
modelControlType: "color"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加不带控制类型的模型
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "框架",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 替换模型
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 替换模型并指定控制类型
|
||||||
|
kernel.model.replace({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/新卷帘.glb",
|
||||||
|
modelControlType: "rotation"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 模型变换 (Transform)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 设置模型旋转 - 直接使用角度(默认)
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
vector3: { x: 0, y: 90, z: 0 } // 绕Y轴旋转90度
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更多角度示例
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
vector3: { x: 0, y: 180, z: 0 } // 旋转180度
|
||||||
|
});
|
||||||
|
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
vector3: { x: 45, y: 90, z: 0 } // X轴45度,Y轴90度
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果需要使用弧度,设置 useDegrees: false
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
vector3: { x: 0, y: Math.PI / 2, z: 0 },
|
||||||
|
useDegrees: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置模型位置
|
||||||
|
kernel.transform.position({
|
||||||
|
modelId: "小桌",
|
||||||
|
vector3: { x: 10, y: 0, z: 5 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置模型缩放
|
||||||
|
kernel.transform.scale({
|
||||||
|
modelId: "框架",
|
||||||
|
vector3: { x: 1.5, y: 1.5, z: 1.5 } // 放大1.5倍
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 点击事件回调
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
kernel.on('model:click', (data) => {
|
||||||
|
console.log('点击的网格名称:', data.meshName);
|
||||||
|
console.log('点击的网格对象:', data.pickedMesh);
|
||||||
|
console.log('点击的3D坐标:', data.pickedPoint);
|
||||||
|
console.log('材质名称:', data.materialName);
|
||||||
|
console.log('模型控制类型:', data.modelControlType); // 'rotation' | 'color' | undefined
|
||||||
|
|
||||||
|
// 根据控制类型执行不同操作
|
||||||
|
if (data.modelControlType === 'rotation') {
|
||||||
|
console.log('这是一个可旋转的模型');
|
||||||
|
// 执行旋转相关操作 - 直接使用角度
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
vector3: { x: 0, y: 180, z: 0 }
|
||||||
|
});
|
||||||
|
} else if (data.modelControlType === 'color') {
|
||||||
|
console.log('这是一个可改变颜色的模型');
|
||||||
|
// 执行颜色相关操作
|
||||||
|
kernel.material.color(data.materialName, '#FF0000');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## ModelControlType 说明
|
||||||
|
|
||||||
|
- `rotation`: 表示该模型支持旋转控制
|
||||||
|
- `color`: 表示该模型支持颜色控制
|
||||||
|
- `undefined`: 未指定控制类型
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { kernel } from './index.js';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
container: document.querySelector('#renderDom'),
|
||||||
|
modelUrlList: [],
|
||||||
|
env: {
|
||||||
|
envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env',
|
||||||
|
intensity: 1.2,
|
||||||
|
rotationY: 0.3,
|
||||||
|
background: true
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
kernel.init(config);
|
||||||
|
|
||||||
|
// 添加多个模型
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "框架",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb"
|
||||||
|
});
|
||||||
|
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘大.glb",
|
||||||
|
modelControlType: "rotation"
|
||||||
|
});
|
||||||
|
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "小桌",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
||||||
|
modelControlType: "color"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模型加载完成后设置变换
|
||||||
|
kernel.on('model:loaded', () => {
|
||||||
|
// 设置初始位置
|
||||||
|
kernel.transform.position({
|
||||||
|
modelId: "小桌",
|
||||||
|
vector3: { x: 5, y: 0, z: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置初始旋转 - 直接使用角度
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
vector3: { x: 0, y: 45, z: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置缩放
|
||||||
|
kernel.transform.scale({
|
||||||
|
modelId: "框架",
|
||||||
|
vector3: { x: 1.2, y: 1.2, z: 1.2 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听点击事件
|
||||||
|
kernel.on('model:click', (data) => {
|
||||||
|
console.log('模型点击数据:', data);
|
||||||
|
|
||||||
|
if (data.modelControlType === 'rotation') {
|
||||||
|
// 处理旋转逻辑 - 每次点击旋转45度
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: data.meshName,
|
||||||
|
vector3: { x: 0, y: 45, z: 0 }
|
||||||
|
});
|
||||||
|
} else if (data.modelControlType === 'color') {
|
||||||
|
// 处理颜色变更逻辑
|
||||||
|
kernel.material.color(data.materialName, '#FF0000');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transform API 详细说明
|
||||||
|
|
||||||
|
### rotation - 旋转
|
||||||
|
- 参数:`{ modelId: string, vector3: { x, y, z }, useDegrees?: boolean }`
|
||||||
|
- **默认使用角度**:直接传递 90、180 等角度值
|
||||||
|
- 如需使用弧度,设置 `useDegrees: false`
|
||||||
|
- 示例:
|
||||||
|
```javascript
|
||||||
|
// 使用角度(默认,推荐)
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "model1",
|
||||||
|
vector3: { x: 0, y: 90, z: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用弧度
|
||||||
|
kernel.transform.rotation({
|
||||||
|
modelId: "model1",
|
||||||
|
vector3: { x: 0, y: Math.PI / 2, z: 0 },
|
||||||
|
useDegrees: false
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### position - 位置
|
||||||
|
- 参数:`{ modelId: string, vector3: { x, y, z } }`
|
||||||
|
- 单位:场景单位
|
||||||
|
- 坐标系:右手坐标系(X右,Y上,Z前)
|
||||||
|
|
||||||
|
### scale - 缩放
|
||||||
|
- 参数:`{ modelId: string, vector3: { x, y, z } }`
|
||||||
|
- 单位:倍数(1.0 = 原始大小)
|
||||||
|
- 可以设置不同轴向的缩放比例
|
||||||
@ -15,10 +15,25 @@ const config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
kernel.model.add("卷帘大", "https://sdk.zguiy.com/resurces/model/框架.glb")
|
kernel.model.add({
|
||||||
kernel.model.add("卷帘大", "https://sdk.zguiy.com/resurces/model/卷帘大.glb")
|
modelId: "框架",
|
||||||
kernel.model.add("卷帘小", "https://sdk.zguiy.com/resurces/model/卷帘小.glb")
|
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb"
|
||||||
kernel.model.add("小桌", "https://sdk.zguiy.com/resurces/model/小桌.glb")
|
});
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "卷帘大",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘大.glb",
|
||||||
|
modelControlType: "rotation"
|
||||||
|
});
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "卷帘小",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/卷帘小.glb",
|
||||||
|
modelControlType: "rotation"
|
||||||
|
});
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: "小桌",
|
||||||
|
modelUrl: "https://sdk.zguiy.com/resurces/model/小桌.glb",
|
||||||
|
modelControlType: "color"
|
||||||
|
});
|
||||||
|
|
||||||
kernel.on('model:load:progress', (data) => {
|
kernel.on('model:load:progress', (data) => {
|
||||||
console.log('模型加载事件', data);
|
console.log('模型加载事件', data);
|
||||||
@ -64,7 +79,7 @@ let currentPickedMesh = null;
|
|||||||
|
|
||||||
kernel.on('model:click', (data) => {
|
kernel.on('model:click', (data) => {
|
||||||
console.log('模型点击事件', data);
|
console.log('模型点击事件', data);
|
||||||
console.log(data);
|
console.log('模型控制类型:', data.modelControlType);
|
||||||
|
|
||||||
// DOM 2D转3D 示例:点击模型时显示信息框
|
// DOM 2D转3D 示例:点击模型时显示信息框
|
||||||
if (data.pickedMesh && data.pickedPoint) {
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
|
|||||||
73
index.html
73
index.html
@ -315,7 +315,8 @@
|
|||||||
<div id="info-name" style="margin-bottom: 5px;">名称: -</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="info-position" style="margin-bottom: 10px; font-size: 12px; color: #666;">坐标: -</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
<!-- 颜色按钮 -->
|
||||||
|
<div id="color-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
||||||
<button id="color-btn-1" style="
|
<button id="color-btn-1" style="
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -338,6 +339,30 @@
|
|||||||
">黑色</button>
|
">黑色</button>
|
||||||
</div>
|
</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;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button id="remove-model-btn" style="
|
<button id="remove-model-btn" style="
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -419,7 +444,11 @@
|
|||||||
const modelUrl = `https://sdk.zguiy.com/resurces/model/${currentText}.glb`;
|
const modelUrl = `https://sdk.zguiy.com/resurces/model/${currentText}.glb`;
|
||||||
console.log('替换百叶模型:', modelUrl);
|
console.log('替换百叶模型:', modelUrl);
|
||||||
try {
|
try {
|
||||||
await kernel.model.replace('卷帘小', modelUrl);
|
await kernel.model.replace({
|
||||||
|
modelId: '卷帘小',
|
||||||
|
modelUrl: modelUrl,
|
||||||
|
modelControlType: 'color'
|
||||||
|
});
|
||||||
console.log(`百叶模型已替换为 ${currentText}`);
|
console.log(`百叶模型已替换为 ${currentText}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`百叶模型替换失败:`, error);
|
console.error(`百叶模型替换失败:`, error);
|
||||||
@ -469,7 +498,7 @@
|
|||||||
// 这里可以根据配置变更来操作 3D 模型
|
// 这里可以根据配置变更来操作 3D 模型
|
||||||
// 例如:
|
// 例如:
|
||||||
// if (e.detail.category === 'size') {
|
// if (e.detail.category === 'size') {
|
||||||
// kernel.model.replace('shed', `/models/shed-${e.detail.value}.glb`);
|
// kernel.model.replace({ modelId: 'shed', modelUrl: `/models/shed-${e.detail.value}.glb`, modelControlType: 'rotation' });
|
||||||
// }
|
// }
|
||||||
// if (e.detail.category === 'color') {
|
// if (e.detail.category === 'color') {
|
||||||
// kernel.material.apply({
|
// kernel.material.apply({
|
||||||
@ -509,6 +538,44 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 旋转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.addRotation({
|
||||||
|
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.addRotation({
|
||||||
|
modelId: modelName,
|
||||||
|
vector3: { x: 0, y: 180, z: 0 }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('未找到模型名称');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('没有选中的网格');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 移除按钮事件
|
// 移除按钮事件
|
||||||
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
const pickedMesh = window.getCurrentPickedMesh();
|
||||||
|
|||||||
104
index.js
104
index.js
@ -12,13 +12,40 @@ const config = {
|
|||||||
container: document.querySelector('#renderDom'),
|
container: document.querySelector('#renderDom'),
|
||||||
modelUrlList: [],
|
modelUrlList: [],
|
||||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
||||||
|
gizmo: {
|
||||||
|
position: true,
|
||||||
|
rotation: true,
|
||||||
|
scale: false
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
enable: true,
|
||||||
|
color: "#2196F3",
|
||||||
|
thickness:1,
|
||||||
|
occlusionStrength:0.1,
|
||||||
|
occlusionThreshold:0.0002
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
kernel.model.add("卷帘大", "https://sdk.zguiy.com/resurces/model/框架.glb")
|
kernel.model.add({
|
||||||
kernel.model.add("卷帘大", "https://sdk.zguiy.com/resurces/model/卷帘大.glb")
|
modelId: "框架",
|
||||||
kernel.model.add("卷帘小", "https://sdk.zguiy.com/resurces/model/卷帘小.glb")
|
modelUrl: "https://sdk.zguiy.com/resurces/model/框架.glb"
|
||||||
kernel.model.add("小桌", "https://sdk.zguiy.com/resurces/model/小桌.glb")
|
});
|
||||||
|
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);
|
||||||
@ -64,28 +91,55 @@ let currentPickedMesh = null;
|
|||||||
|
|
||||||
kernel.on('model:click', (data) => {
|
kernel.on('model:click', (data) => {
|
||||||
console.log('模型点击事件', data);
|
console.log('模型点击事件', data);
|
||||||
console.log(data);
|
console.log('模型控制类型:', data.modelControlType);
|
||||||
|
switch (data.modelControlType) {
|
||||||
|
case "color":
|
||||||
|
// DOM 2D转3D 示例:点击模型时显示信息框
|
||||||
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
|
const meshName = data.pickedMesh.name;
|
||||||
|
const position = data.pickedPoint; // 使用点击位置的坐标
|
||||||
|
currentMaterialName = data.materialName || ''; // 保存材质名
|
||||||
|
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
||||||
|
|
||||||
// DOM 2D转3D 示例:点击模型时显示信息框
|
// 获取已创建的DOM元素
|
||||||
if (data.pickedMesh && data.pickedPoint) {
|
const infoDiv = document.getElementById('model-info-box');
|
||||||
const meshName = data.pickedMesh.name;
|
// 更新信息内容
|
||||||
const position = data.pickedPoint; // 使用点击位置的坐标
|
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
||||||
currentMaterialName = data.materialName || ''; // 保存材质名
|
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
||||||
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
|
||||||
|
|
||||||
console.log('点击位置的3D坐标:', position);
|
// 显示颜色按钮,隐藏旋转按钮
|
||||||
console.log('材质名:', currentMaterialName);
|
document.getElementById('color-buttons').style.display = 'flex';
|
||||||
|
document.getElementById('rotation-buttons').style.display = 'none';
|
||||||
|
|
||||||
// 获取已创建的DOM元素
|
// 将DOM附加到点击的3D坐标(会自动显示)
|
||||||
const infoDiv = document.getElementById('model-info-box');
|
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "rotation":
|
||||||
|
// 显示旋转控制UI
|
||||||
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
|
const meshName = data.pickedMesh.name;
|
||||||
|
const position = data.pickedPoint;
|
||||||
|
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
||||||
|
|
||||||
// 更新信息内容
|
// 获取已创建的DOM元素
|
||||||
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
const infoDiv = document.getElementById('model-info-box');
|
||||||
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
// 更新信息内容
|
||||||
|
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
||||||
|
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
||||||
|
|
||||||
// 将DOM附加到点击的3D坐标(会自动显示)
|
// 显示旋转按钮,隐藏颜色按钮
|
||||||
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
document.getElementById('rotation-buttons').style.display = 'flex';
|
||||||
|
document.getElementById('color-buttons').style.display = 'none';
|
||||||
|
|
||||||
|
// 将DOM附加到点击的3D坐标(会自动显示)
|
||||||
|
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 暴露到全局,供 index.html 使用
|
// 暴露到全局,供 index.html 使用
|
||||||
@ -109,12 +163,12 @@ kernel.on('hotspot:click', (data) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.kernel = kernel;
|
window.kernel = kernel;
|
||||||
// 添加模型到场景
|
// 添加模型到场景示例
|
||||||
// await kernel.model.add('https://sdk.zguiy.com/resurces/model/百叶1.glb');
|
// await kernel.model.add({ modelId: '百叶1', modelUrl: 'https://sdk.zguiy.com/resurces/model/百叶1.glb', modelControlType: 'rotation' });
|
||||||
|
|
||||||
// 销毁模型
|
// 销毁模型
|
||||||
// kernel.model.destroy('car');
|
// kernel.model.removeByName('car');
|
||||||
|
|
||||||
// 替换模型
|
// 替换模型示例
|
||||||
// await kernel.model.replace('car', '/models/new-car.glb');
|
// await kernel.model.replace({ modelId: 'car', modelUrl: '/models/new-car.glb', modelControlType: 'color' });
|
||||||
|
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@ -8,8 +8,8 @@
|
|||||||
"name": "client-babylonjs-pure",
|
"name": "client-babylonjs-pure",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babylonjs/core": "^7.0.0",
|
"@babylonjs/core": "^9.3.1",
|
||||||
"@babylonjs/loaders": "^7.0.0",
|
"@babylonjs/loaders": "^9.3.1",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@ -23,19 +23,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babylonjs/core": {
|
"node_modules/@babylonjs/core": {
|
||||||
"version": "7.54.3",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-7.54.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-9.3.1.tgz",
|
||||||
"integrity": "sha512-P5ncXVd8GEUJLhwloP9V0oVwQYIrvZztguVeLlvd5Rx+9aQnenKjpV8auJ6SRsUlAmNZU4pFTKzwF6o2EUfhAw==",
|
"integrity": "sha512-gCAVsS40EF9SFXUoe5wl5lA03hwmRQoP9v3y8EdQ2aPSaozIApu4LrxI6yFgczzxGVa2utcj6rF6pgO5VuK7nw==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@babylonjs/loaders": {
|
"node_modules/@babylonjs/loaders": {
|
||||||
"version": "7.54.3",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-7.54.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-9.3.1.tgz",
|
||||||
"integrity": "sha512-RBPmOsaMTxi6Ga08ueLTm6Tnvx/l2nNQigucubvrngZ7muwn5/ubfcStckkI1c0qvhR1+/FFlD54do7gZ1pnsQ==",
|
"integrity": "sha512-rzXjBHARqh5MUZFltA26mX5NwQJtn9Wu1dR3Rch3sXZD9ShDBpUcEcdl7/LqbatysB9PKEN99tTN/ljwEUlRww==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babylonjs/core": "^7.0.0",
|
"@babylonjs/core": "^9.0.0",
|
||||||
"babylonjs-gltf2interface": "^7.0.0"
|
"babylonjs-gltf2interface": "^9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
@ -854,9 +854,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babylonjs-gltf2interface": {
|
"node_modules/babylonjs-gltf2interface": {
|
||||||
"version": "7.54.3",
|
"version": "9.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-7.54.3.tgz",
|
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.4.1.tgz",
|
||||||
"integrity": "sha512-ZAWYFyE+SOczfWT19O4e3YRkCZ5i57SiD2eK2kqc+Tow/t9X1S45xgSFNuHZff++dd5BlVIEQDSnFV+McFLSnQ==",
|
"integrity": "sha512-4yWrVlOJIea1KF5TXqiPq8iz/mSogXf5e4DES73N290Cm4kyUfng5mmdBljnrhoM1KeXTK3PYilOP9157cdNQg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,11 +8,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babylonjs/core": "^7.0.0",
|
"@babylonjs/core": "^9.3.1",
|
||||||
"@babylonjs/loaders": "^7.0.0",
|
"@babylonjs/loaders": "^9.3.1",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"ws": "^8.14.0"
|
"ws": "^8.14.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,5 +13,17 @@ export const AppConfig = {
|
|||||||
intensity: 1.5,
|
intensity: 1.5,
|
||||||
rotationY: 0,
|
rotationY: 0,
|
||||||
background: true,
|
background: true,
|
||||||
}
|
},
|
||||||
|
gizmo: {
|
||||||
|
position: true,
|
||||||
|
rotation: false,
|
||||||
|
scale: false,
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
enable: true,
|
||||||
|
color: '#2196F3',
|
||||||
|
thickness: 3.0,
|
||||||
|
occlusionStrength: 0.9,
|
||||||
|
occlusionThreshold: 0.0002,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
|
import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader';
|
||||||
import '@babylonjs/loaders/glTF';
|
import '@babylonjs/loaders/glTF';
|
||||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||||
|
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
||||||
|
import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector';
|
||||||
import { Scene } from '@babylonjs/core/scene';
|
import { Scene } from '@babylonjs/core/scene';
|
||||||
import { Monobehiver } from '../base/Monobehiver';
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
import { Dictionary } from '../utils/Dictionary';
|
import { Dictionary } from '../utils/Dictionary';
|
||||||
@ -19,17 +21,27 @@ type ModelConfig = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ModelControlType = 'rotation' | 'color';
|
||||||
|
|
||||||
|
type ModelMetadata = {
|
||||||
|
modelId: string;
|
||||||
|
modelUrl: string;
|
||||||
|
modelControlType?: ModelControlType;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模型管理类 - 负责加载、缓存和管理3D模型
|
* 模型管理类 - 负责加载、缓存和管理3D模型
|
||||||
*/
|
*/
|
||||||
export class AppModel extends Monobehiver {
|
export class AppModel extends Monobehiver {
|
||||||
private modelDic: Dictionary<AbstractMesh[]>;
|
private modelDic: Dictionary<AbstractMesh[]>;
|
||||||
|
private modelMetadataDic: Dictionary<ModelMetadata>;
|
||||||
private loadedMeshes: AbstractMesh[];
|
private loadedMeshes: AbstractMesh[];
|
||||||
private isLoading: boolean;
|
private isLoading: boolean;
|
||||||
|
|
||||||
constructor(mainApp: any) {
|
constructor(mainApp: any) {
|
||||||
super(mainApp);
|
super(mainApp);
|
||||||
this.modelDic = new Dictionary<AbstractMesh[]>();
|
this.modelDic = new Dictionary<AbstractMesh[]>();
|
||||||
|
this.modelMetadataDic = new Dictionary<ModelMetadata>();
|
||||||
this.loadedMeshes = [];
|
this.loadedMeshes = [];
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@ -143,6 +155,40 @@ export class AppModel extends Monobehiver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
/** 为网格设置阴影(投射和接收) */
|
/** 为网格设置阴影(投射和接收) */
|
||||||
|
private createModelRoot(modelId: string, meshes: AbstractMesh[]): AbstractMesh[] {
|
||||||
|
const scene = this.mainApp.appScene.object;
|
||||||
|
const root = new Mesh(`${modelId}__root`, scene);
|
||||||
|
const meshSet = new Set<AbstractMesh>(meshes);
|
||||||
|
root.position.copyFrom(this.getMeshesBoundingCenter(meshes));
|
||||||
|
|
||||||
|
meshes.forEach(mesh => {
|
||||||
|
if (!mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)) {
|
||||||
|
mesh.setParent(root, true, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadedMeshes.push(root);
|
||||||
|
return [root, ...meshes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMeshesBoundingCenter(meshes: AbstractMesh[]): Vector3 {
|
||||||
|
const renderableMeshes = meshes.filter(mesh => !mesh.isDisposed() && mesh.getTotalVertices() > 0);
|
||||||
|
if (!renderableMeshes.length) return Vector3.Zero();
|
||||||
|
|
||||||
|
const min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
|
||||||
|
const max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
|
||||||
|
|
||||||
|
renderableMeshes.forEach(mesh => {
|
||||||
|
mesh.computeWorldMatrix(true);
|
||||||
|
const boundingBox = mesh.getBoundingInfo().boundingBox;
|
||||||
|
min.minimizeInPlace(boundingBox.minimumWorld);
|
||||||
|
max.maximizeInPlace(boundingBox.maximumWorld);
|
||||||
|
});
|
||||||
|
|
||||||
|
return min.add(max).scaleInPlace(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
setupShadows(meshes: AbstractMesh[]): void {
|
setupShadows(meshes: AbstractMesh[]): void {
|
||||||
const appLight = this.mainApp.appLight;
|
const appLight = this.mainApp.appLight;
|
||||||
if (!appLight) return;
|
if (!appLight) return;
|
||||||
@ -170,30 +216,28 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加模型到场景(支持单个或批量)
|
* 添加模型到场景(支持单个或批量)
|
||||||
* @param modelName 模型名称 或 模型配置数组
|
* @param modelConfig 模型配置对象 或 模型配置数组
|
||||||
* @param modelUrl 模型URL(单个模型时使用)
|
|
||||||
*/
|
*/
|
||||||
async add(
|
async add(
|
||||||
modelName: string | ModelConfig[],
|
modelConfig: ModelMetadata | ModelMetadata[]
|
||||||
modelUrl?: string
|
|
||||||
): Promise<LoadResult | { success: boolean; results: LoadResult[] }> {
|
): Promise<LoadResult | { success: boolean; results: LoadResult[] }> {
|
||||||
// 批量加载
|
// 批量加载
|
||||||
if (Array.isArray(modelName)) {
|
if (Array.isArray(modelConfig)) {
|
||||||
return await this.addMultiple(modelName);
|
return await this.addMultiple(modelConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单个加载
|
// 单个加载
|
||||||
if (!modelUrl) {
|
return await this.addSingle(
|
||||||
return { success: false, error: '缺少模型URL参数' };
|
modelConfig.modelId,
|
||||||
}
|
modelConfig.modelUrl,
|
||||||
|
modelConfig.modelControlType
|
||||||
return await this.addSingle(modelName, modelUrl);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加单个模型
|
* 添加单个模型
|
||||||
*/
|
*/
|
||||||
private async addSingle(modelName: string, modelUrl: string): Promise<LoadResult> {
|
private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType): 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()) {
|
||||||
@ -209,8 +253,16 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
if (result.success && result.meshes) {
|
if (result.success && result.meshes) {
|
||||||
// this.cloneMaterials(result.meshes, modelName);
|
// this.cloneMaterials(result.meshes, modelName);
|
||||||
|
result.meshes = this.createModelRoot(modelName, result.meshes);
|
||||||
this.modelDic.Set(modelName, result.meshes);
|
this.modelDic.Set(modelName, result.meshes);
|
||||||
|
|
||||||
|
// 存储元数据
|
||||||
|
this.modelMetadataDic.Set(modelName, {
|
||||||
|
modelId: modelName,
|
||||||
|
modelUrl: modelUrl,
|
||||||
|
modelControlType: modelControlType
|
||||||
|
});
|
||||||
|
|
||||||
// 更新 GameManager 的字典
|
// 更新 GameManager 的字典
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
this.mainApp.gameManager?.updateDictionaries();
|
||||||
|
|
||||||
@ -225,32 +277,40 @@ export class AppModel extends Monobehiver {
|
|||||||
/**
|
/**
|
||||||
* 批量添加模型
|
* 批量添加模型
|
||||||
*/
|
*/
|
||||||
private async addMultiple(models: ModelConfig[]): Promise<{ success: boolean; results: LoadResult[] }> {
|
private async addMultiple(models: ModelMetadata[]): Promise<{ success: boolean; results: LoadResult[] }> {
|
||||||
const total = models.length;
|
const total = models.length;
|
||||||
const results: LoadResult[] = [];
|
const results: LoadResult[] = [];
|
||||||
|
|
||||||
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 { name, url } = models[i];
|
const { modelId, modelUrl, modelControlType } = models[i];
|
||||||
|
|
||||||
const result = await this.loadSingleModel(url, (event) => {
|
const result = await this.loadSingleModel(modelUrl, (event) => {
|
||||||
this.emitProgress(i, total, url, event);
|
this.emitProgress(i, total, modelUrl, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success && result.meshes) {
|
if (result.success && result.meshes) {
|
||||||
this.cloneMaterials(result.meshes, name);
|
result.meshes = this.createModelRoot(modelId, result.meshes);
|
||||||
this.modelDic.Set(name, result.meshes);
|
this.cloneMaterials(result.meshes, modelId);
|
||||||
|
this.modelDic.Set(modelId, result.meshes);
|
||||||
|
|
||||||
|
// 存储元数据
|
||||||
|
this.modelMetadataDic.Set(modelId, {
|
||||||
|
modelId: modelId,
|
||||||
|
modelUrl: modelUrl,
|
||||||
|
modelControlType: modelControlType
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(result);
|
results.push(result);
|
||||||
this.emitProgress(i + 1, total, url, null, result.success);
|
this.emitProgress(i + 1, total, modelUrl, null, result.success);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量加载完成后统一更新字典
|
// 批量加载完成后统一更新字典
|
||||||
this.mainApp.gameManager?.updateDictionaries();
|
this.mainApp.gameManager?.updateDictionaries();
|
||||||
|
|
||||||
EventBridge.modelLoaded({ urls: models.map(m => m.url) });
|
EventBridge.modelLoaded({ urls: models.map(m => m.modelUrl) });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: results.every(r => r.success),
|
success: results.every(r => r.success),
|
||||||
@ -352,14 +412,17 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 替换模型
|
* 替换模型
|
||||||
* @param modelName 模型名称
|
* @param modelConfig 模型配置对象
|
||||||
* @param newModelUrl 新模型URL
|
|
||||||
*/
|
*/
|
||||||
async replaceModel(modelName: string, newModelUrl: string): Promise<LoadResult> {
|
async replaceModel(modelConfig: ModelMetadata): Promise<LoadResult> {
|
||||||
console.log( modelName,this.modelDic);
|
console.log(modelConfig.modelId, this.modelDic);
|
||||||
|
|
||||||
this.removeByName(modelName);
|
this.removeByName(modelConfig.modelId);
|
||||||
return await this.addSingle(modelName, newModelUrl);
|
return await this.addSingle(
|
||||||
|
modelConfig.modelId,
|
||||||
|
modelConfig.modelUrl,
|
||||||
|
modelConfig.modelControlType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -375,6 +438,151 @@ export class AppModel extends Monobehiver {
|
|||||||
|
|
||||||
meshes.forEach(mesh => mesh.dispose());
|
meshes.forEach(mesh => mesh.dispose());
|
||||||
this.modelDic.Remove(modelName);
|
this.modelDic.Remove(modelName);
|
||||||
|
this.modelMetadataDic.Remove(modelName);
|
||||||
console.log(`Model removed: ${modelName}`);
|
console.log(`Model removed: ${modelName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型元数据
|
||||||
|
* @param modelName 模型名称
|
||||||
|
*/
|
||||||
|
getModelMetadata(modelName: string): ModelMetadata | undefined {
|
||||||
|
return this.modelMetadataDic.Get(modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据网格查找模型元数据
|
||||||
|
* @param mesh 网格对象
|
||||||
|
*/
|
||||||
|
getMetadataByMesh(mesh: AbstractMesh): ModelMetadata | undefined {
|
||||||
|
const modelName = this.findModelNameByMesh(mesh);
|
||||||
|
if (modelName) {
|
||||||
|
return this.modelMetadataDic.Get(modelName);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModelTransformTargets(meshes: AbstractMesh[]): AbstractMesh[] {
|
||||||
|
const meshSet = new Set<AbstractMesh>(meshes);
|
||||||
|
const rootMeshes = meshes.filter(mesh => !mesh.parent || !meshSet.has(mesh.parent as AbstractMesh));
|
||||||
|
|
||||||
|
return rootMeshes.length ? rootMeshes : meshes.slice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getModelTransformTargetByMesh(mesh: AbstractMesh): AbstractMesh | undefined {
|
||||||
|
const modelName = this.findModelNameByMesh(mesh);
|
||||||
|
if (!modelName) return mesh;
|
||||||
|
|
||||||
|
const meshes = this.modelDic.Get(modelName);
|
||||||
|
if (!meshes?.length) return mesh;
|
||||||
|
|
||||||
|
return this.getModelTransformTargets(meshes)[0] ?? mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
getModelMeshesByMesh(mesh: AbstractMesh): AbstractMesh[] {
|
||||||
|
const modelName = this.findModelNameByMesh(mesh);
|
||||||
|
if (!modelName) return [mesh];
|
||||||
|
|
||||||
|
const meshes = this.modelDic.Get(modelName);
|
||||||
|
return meshes?.length ? meshes : [mesh];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模型旋转
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param rotation 旋转向量 {x, y, z}(默认使用角度)
|
||||||
|
* @param useDegrees 是否使用角度(默认true)
|
||||||
|
*/
|
||||||
|
setRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void {
|
||||||
|
const meshes = this.modelDic.Get(modelId);
|
||||||
|
if (!meshes?.length) {
|
||||||
|
console.warn(`Model not found: ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果使用角度,转换为弧度
|
||||||
|
const toRadians = (degrees: number) => degrees * Math.PI / 180;
|
||||||
|
const rotationValues = useDegrees ? {
|
||||||
|
x: toRadians(rotation.x),
|
||||||
|
y: toRadians(rotation.y),
|
||||||
|
z: toRadians(rotation.z)
|
||||||
|
} : rotation;
|
||||||
|
|
||||||
|
this.getModelTransformTargets(meshes).forEach(mesh => {
|
||||||
|
if (mesh.rotationQuaternion) {
|
||||||
|
mesh.rotationQuaternion = Quaternion.FromEulerAngles(
|
||||||
|
rotationValues.x,
|
||||||
|
rotationValues.y,
|
||||||
|
rotationValues.z
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.rotation.set(rotationValues.x, rotationValues.y, rotationValues.z);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 累加模型旋转
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param rotation 旋转向量 {x, y, z}(默认使用角度)
|
||||||
|
* @param useDegrees 是否使用角度(默认true)
|
||||||
|
*/
|
||||||
|
addRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void {
|
||||||
|
const meshes = this.modelDic.Get(modelId);
|
||||||
|
if (!meshes?.length) {
|
||||||
|
console.warn(`Model not found: ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果使用角度,转换为弧度
|
||||||
|
const toRadians = (degrees: number) => degrees * Math.PI / 180;
|
||||||
|
const rotationValues = useDegrees ? {
|
||||||
|
x: toRadians(rotation.x),
|
||||||
|
y: toRadians(rotation.y),
|
||||||
|
z: toRadians(rotation.z)
|
||||||
|
} : rotation;
|
||||||
|
|
||||||
|
this.getModelTransformTargets(meshes).forEach(mesh => {
|
||||||
|
mesh.addRotation(rotationValues.x, rotationValues.y, rotationValues.z);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模型位置
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param position 位置向量 {x, y, z}
|
||||||
|
*/
|
||||||
|
setPosition(modelId: string, position: { x: number; y: number; z: number }): void {
|
||||||
|
const meshes = this.modelDic.Get(modelId);
|
||||||
|
if (!meshes?.length) {
|
||||||
|
console.warn(`Model not found: ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getModelTransformTargets(meshes).forEach(mesh => {
|
||||||
|
mesh.position.x = position.x;
|
||||||
|
mesh.position.y = position.y;
|
||||||
|
mesh.position.z = position.z;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模型缩放
|
||||||
|
* @param modelId 模型ID
|
||||||
|
* @param scale 缩放向量 {x, y, z}
|
||||||
|
*/
|
||||||
|
setScale(modelId: string, scale: { x: number; y: number; z: number }): void {
|
||||||
|
const meshes = this.modelDic.Get(modelId);
|
||||||
|
if (!meshes?.length) {
|
||||||
|
console.warn(`Model not found: ${modelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getModelTransformTargets(meshes).forEach(mesh => {
|
||||||
|
mesh.scaling.x = scale.x;
|
||||||
|
mesh.scaling.y = scale.y;
|
||||||
|
mesh.scaling.z = scale.z;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/babylonjs/AppPositionGizmo.ts
Normal file
80
src/babylonjs/AppPositionGizmo.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||||
|
import { PositionGizmo } from '@babylonjs/core/Gizmos/positionGizmo';
|
||||||
|
import { UtilityLayerRenderer } from '@babylonjs/core/Rendering/utilityLayerRenderer';
|
||||||
|
import { MainApp } from './MainApp';
|
||||||
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
|
|
||||||
|
export class AppPositionGizmo extends Monobehiver {
|
||||||
|
private utilityLayer: UtilityLayerRenderer | null = null;
|
||||||
|
private gizmo: PositionGizmo | null = null;
|
||||||
|
private enabled = true;
|
||||||
|
private rotationEnabled = false;
|
||||||
|
private scaleEnabled = false;
|
||||||
|
|
||||||
|
constructor(mainApp: MainApp) {
|
||||||
|
super(mainApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Awake(): void {
|
||||||
|
const scene = this.mainApp.appScene.object;
|
||||||
|
if (!scene) return;
|
||||||
|
|
||||||
|
this.utilityLayer = new UtilityLayerRenderer(scene);
|
||||||
|
this.gizmo = new PositionGizmo(this.utilityLayer);
|
||||||
|
this.gizmo.updateGizmoRotationToMatchAttachedMesh = false;
|
||||||
|
this.gizmo.updateGizmoPositionToMatchAttachedMesh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnabled(enabled: boolean): void {
|
||||||
|
this.enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
this.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(options?: { position?: boolean; rotation?: boolean; scale?: boolean }): void {
|
||||||
|
if (!options) return;
|
||||||
|
|
||||||
|
if (typeof options.position === 'boolean') {
|
||||||
|
this.setEnabled(options.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.rotation === 'boolean') {
|
||||||
|
this.rotationEnabled = options.rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.scale === 'boolean') {
|
||||||
|
this.scaleEnabled = options.scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(): void {
|
||||||
|
this.setEnabled(!this.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
attach(mesh: AbstractMesh | null): void {
|
||||||
|
if (!this.enabled || !this.gizmo) return;
|
||||||
|
this.gizmo.attachedMesh = mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
detach(): void {
|
||||||
|
if (this.gizmo) {
|
||||||
|
this.gizmo.attachedMesh = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttachedMesh(): AbstractMesh | null {
|
||||||
|
return this.gizmo?.attachedMesh ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.gizmo?.dispose();
|
||||||
|
this.utilityLayer?.dispose();
|
||||||
|
this.gizmo = null;
|
||||||
|
this.utilityLayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -87,17 +87,35 @@ class AppRay extends Monobehiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mainApp.appDomTo3D.hideAll()
|
||||||
|
|
||||||
const materialName = pickInfo.pickedMesh.material?.name || '';
|
const materialName = pickInfo.pickedMesh.material?.name || '';
|
||||||
|
const holdingShift = Boolean((evt as any).shiftKey);
|
||||||
|
const modelMeshes = this.mainApp.appModel.getModelMeshesByMesh(pickInfo.pickedMesh);
|
||||||
|
if (holdingShift) {
|
||||||
|
this.mainApp.appSelectionOutline.toggle(modelMeshes);
|
||||||
|
} else {
|
||||||
|
this.mainApp.appSelectionOutline.select(modelMeshes);
|
||||||
|
}
|
||||||
|
const transformTarget = this.mainApp.appModel.getModelTransformTargetByMesh(pickInfo.pickedMesh);
|
||||||
|
this.mainApp.appPositionGizmo.attach(transformTarget ?? pickInfo.pickedMesh);
|
||||||
|
|
||||||
|
// 获取模型元数据
|
||||||
|
const modelMetadata = this.mainApp.appModel.getMetadataByMesh(pickInfo.pickedMesh);
|
||||||
|
|
||||||
EventBridge.modelClick({
|
EventBridge.modelClick({
|
||||||
meshName: pickInfo.pickedMesh.name,
|
meshName: pickInfo.pickedMesh.name,
|
||||||
pickedMesh: pickInfo.pickedMesh,
|
pickedMesh: pickInfo.pickedMesh,
|
||||||
pickedPoint: pickInfo.pickedPoint,
|
pickedPoint: pickInfo.pickedPoint,
|
||||||
materialName: materialName,
|
materialName: materialName,
|
||||||
|
modelControlType: modelMetadata?.modelControlType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
console.log(1111);
|
console.log(1111);
|
||||||
|
|
||||||
|
this.mainApp.appSelectionOutline.clear();
|
||||||
|
this.mainApp.appPositionGizmo.detach();
|
||||||
this.mainApp.appDomTo3D.hideAll()
|
this.mainApp.appDomTo3D.hideAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
src/babylonjs/AppSelectionOutline.ts
Normal file
159
src/babylonjs/AppSelectionOutline.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||||
|
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
||||||
|
import { SelectionOutlineLayer } from '@babylonjs/core/Layers/selectionOutlineLayer';
|
||||||
|
import '@babylonjs/core/Layers/effectLayerSceneComponent';
|
||||||
|
import { MainApp } from './MainApp';
|
||||||
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
|
|
||||||
|
type OutlineConfig = {
|
||||||
|
enable?: boolean;
|
||||||
|
color?: Color3 | string;
|
||||||
|
thickness?: number;
|
||||||
|
width?: number;
|
||||||
|
occlusionStrength?: number;
|
||||||
|
occlusionThreshold?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AppSelectionOutline extends Monobehiver {
|
||||||
|
private selectedMeshes: AbstractMesh[] = [];
|
||||||
|
private outlineLayer: SelectionOutlineLayer | null = null;
|
||||||
|
private enabled = true;
|
||||||
|
private color = new Color3(0.1, 0.65, 1);
|
||||||
|
private width = 0.08;
|
||||||
|
private occlusionStrength = 0.9;
|
||||||
|
private occlusionThreshold = 0.0002;
|
||||||
|
|
||||||
|
constructor(mainApp: MainApp) {
|
||||||
|
super(mainApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
const scene = this.mainApp.appScene.object;
|
||||||
|
if (!scene || this.outlineLayer) return;
|
||||||
|
|
||||||
|
this.outlineLayer = new SelectionOutlineLayer('selection-outline', scene, {
|
||||||
|
mainTextureRatio: 1,
|
||||||
|
});
|
||||||
|
this.applyLayerConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnabled(enabled: boolean): void {
|
||||||
|
this.enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyle(options: { color?: Color3 | string; width?: number }): void {
|
||||||
|
this.configure(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(options?: OutlineConfig): void {
|
||||||
|
if (!options) return;
|
||||||
|
|
||||||
|
if (typeof options.enable === 'boolean') {
|
||||||
|
this.setEnabled(options.enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.color instanceof Color3) {
|
||||||
|
this.color = options.color;
|
||||||
|
} else if (typeof options.color === 'string') {
|
||||||
|
this.color = Color3.FromHexString(options.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.thickness === 'number') {
|
||||||
|
this.width = options.thickness;
|
||||||
|
} else if (typeof options.width === 'number') {
|
||||||
|
this.width = options.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.occlusionStrength === 'number') {
|
||||||
|
this.occlusionStrength = options.occlusionStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.occlusionThreshold === 'number') {
|
||||||
|
this.occlusionThreshold = options.occlusionThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyLayerConfig();
|
||||||
|
this.rebuildLayerSelection(this.selectedMeshes);
|
||||||
|
}
|
||||||
|
|
||||||
|
select(meshes: AbstractMesh | AbstractMesh[], additive = false): void {
|
||||||
|
if (!this.enabled) return;
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
if (!additive) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addGroup(this.normalizeMeshes(meshes));
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(meshes: AbstractMesh | AbstractMesh[]): void {
|
||||||
|
if (!this.enabled) return;
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
const targets = this.normalizeMeshes(meshes);
|
||||||
|
if (targets.length && targets.every(mesh => this.isSelected(mesh))) {
|
||||||
|
this.remove(targets);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addGroup(targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(meshes: AbstractMesh | AbstractMesh[]): void {
|
||||||
|
const targetIds = new Set(this.normalizeMeshes(meshes).map(mesh => mesh.uniqueId));
|
||||||
|
this.selectedMeshes = this.selectedMeshes.filter(item => !targetIds.has(item.uniqueId));
|
||||||
|
this.rebuildLayerSelection(this.selectedMeshes);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.outlineLayer?.clearSelection();
|
||||||
|
this.selectedMeshes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelection(): AbstractMesh[] {
|
||||||
|
return [...this.selectedMeshes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private addGroup(meshes: AbstractMesh[]): void {
|
||||||
|
const newMeshes = meshes.filter(mesh => !this.isSelected(mesh));
|
||||||
|
if (!newMeshes.length) return;
|
||||||
|
|
||||||
|
this.selectedMeshes.push(...newMeshes);
|
||||||
|
this.rebuildLayerSelection(this.selectedMeshes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSelected(mesh: AbstractMesh): boolean {
|
||||||
|
return this.selectedMeshes.some(item => item.uniqueId === mesh.uniqueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeMeshes(meshes: AbstractMesh | AbstractMesh[]): AbstractMesh[] {
|
||||||
|
const input = Array.isArray(meshes) ? meshes : [meshes];
|
||||||
|
const uniqueMeshes = new Map<number, AbstractMesh>();
|
||||||
|
|
||||||
|
input.forEach(mesh => {
|
||||||
|
if (!mesh || mesh.isDisposed() || mesh.metadata?.type === 'hotspot') return;
|
||||||
|
if (!mesh.isEnabled() || mesh.getTotalVertices() <= 0 || !mesh.material) return;
|
||||||
|
uniqueMeshes.set(mesh.uniqueId, mesh);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...uniqueMeshes.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuildLayerSelection(meshes: AbstractMesh[]): void {
|
||||||
|
this.outlineLayer?.clearSelection();
|
||||||
|
meshes.forEach(mesh => this.outlineLayer?.addSelection(mesh));
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyLayerConfig(): void {
|
||||||
|
if (!this.outlineLayer) return;
|
||||||
|
|
||||||
|
this.outlineLayer.outlineColor = this.color;
|
||||||
|
this.outlineLayer.outlineThickness = this.width;
|
||||||
|
this.outlineLayer.occlusionStrength = this.occlusionStrength;
|
||||||
|
this.outlineLayer.occlusionThreshold = this.occlusionThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,8 @@ import { GameManager } from './GameManager';
|
|||||||
import { EventBridge } from '../event/bridge';
|
import { EventBridge } from '../event/bridge';
|
||||||
import { AppHotspot } from './AppHotspot';
|
import { AppHotspot } from './AppHotspot';
|
||||||
import { AppDomTo3D } from './AppDomTo3D';
|
import { AppDomTo3D } from './AppDomTo3D';
|
||||||
|
import { AppSelectionOutline } from './AppSelectionOutline';
|
||||||
|
import { AppPositionGizmo } from './AppPositionGizmo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主应用类 - 3D场景的核心控制器
|
* 主应用类 - 3D场景的核心控制器
|
||||||
@ -30,6 +32,8 @@ export class MainApp {
|
|||||||
appRay: AppRay;
|
appRay: AppRay;
|
||||||
appHotspot: AppHotspot;
|
appHotspot: AppHotspot;
|
||||||
appDomTo3D: AppDomTo3D;
|
appDomTo3D: AppDomTo3D;
|
||||||
|
appSelectionOutline: AppSelectionOutline;
|
||||||
|
appPositionGizmo: AppPositionGizmo;
|
||||||
gameManager: GameManager;
|
gameManager: GameManager;
|
||||||
|
|
||||||
|
|
||||||
@ -43,6 +47,8 @@ export class MainApp {
|
|||||||
this.appRay = new AppRay(this);
|
this.appRay = new AppRay(this);
|
||||||
this.appHotspot = new AppHotspot(this);
|
this.appHotspot = new AppHotspot(this);
|
||||||
this.appDomTo3D = new AppDomTo3D(this);
|
this.appDomTo3D = new AppDomTo3D(this);
|
||||||
|
this.appSelectionOutline = new AppSelectionOutline(this);
|
||||||
|
this.appPositionGizmo = new AppPositionGizmo(this);
|
||||||
this.gameManager = new GameManager(this);
|
this.gameManager = new GameManager(this);
|
||||||
|
|
||||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||||
@ -55,7 +61,11 @@ export class MainApp {
|
|||||||
loadAConfig(config: any): void {
|
loadAConfig(config: any): void {
|
||||||
AppConfig.container = config.container;
|
AppConfig.container = config.container;
|
||||||
AppConfig.modelUrlList = config.modelUrlList || [];
|
AppConfig.modelUrlList = config.modelUrlList || [];
|
||||||
AppConfig.env = config.env;
|
AppConfig.env = { ...AppConfig.env, ...(config.env || {}) };
|
||||||
|
AppConfig.gizmo = { ...AppConfig.gizmo, ...(config.gizmo || {}) };
|
||||||
|
AppConfig.outline = { ...AppConfig.outline, ...(config.outline || {}) };
|
||||||
|
this.appPositionGizmo.configure(AppConfig.gizmo);
|
||||||
|
this.appSelectionOutline.configure(AppConfig.outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadModel(): Promise<void> {
|
async loadModel(): Promise<void> {
|
||||||
@ -73,6 +83,8 @@ export class MainApp {
|
|||||||
this.appLight.Awake();
|
this.appLight.Awake();
|
||||||
this.appEnv.Awake();
|
this.appEnv.Awake();
|
||||||
this.appRay.Awake();
|
this.appRay.Awake();
|
||||||
|
this.appSelectionOutline.init();
|
||||||
|
this.appPositionGizmo.Awake();
|
||||||
this.appDomTo3D.init();
|
this.appDomTo3D.init();
|
||||||
this.appModel.initManagers();
|
this.appModel.initManagers();
|
||||||
this.update();
|
this.update();
|
||||||
@ -93,6 +105,7 @@ export class MainApp {
|
|||||||
async dispose(): Promise<void> {
|
async dispose(): Promise<void> {
|
||||||
this.appModel?.clean();
|
this.appModel?.clean();
|
||||||
this.appEnv?.clean();
|
this.appEnv?.clean();
|
||||||
|
this.appPositionGizmo?.dispose();
|
||||||
// this.appHotspot?.clear();
|
// this.appHotspot?.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,10 @@ export type ModelLoadedPayload = {
|
|||||||
|
|
||||||
export type ModelClickPayload = {
|
export type ModelClickPayload = {
|
||||||
meshName?: string;
|
meshName?: string;
|
||||||
|
pickedMesh?: any;
|
||||||
|
pickedPoint?: any;
|
||||||
|
materialName?: string;
|
||||||
|
modelControlType?: 'rotation' | 'color';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SceneReadyPayload = {
|
export type SceneReadyPayload = {
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import { MainApp } from '../babylonjs/MainApp';
|
import { MainApp } from '../babylonjs/MainApp';
|
||||||
import type { HotspotInput } from '../types/hotspot';
|
import type { HotspotInput } from '../types/hotspot';
|
||||||
|
|
||||||
|
type ModelControlType = 'rotation' | 'color';
|
||||||
|
|
||||||
|
type ModelInput = {
|
||||||
|
modelId: string;
|
||||||
|
modelUrl: string;
|
||||||
|
modelControlType?: ModelControlType;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kernel 转接器类 - 封装 mainApp 的功能,提供统一<E7BB9F>?API 接口
|
* Kernel 转接器类 - 封装 mainApp 的功能,提供统一<E7BB9F>?API 接口
|
||||||
*/
|
*/
|
||||||
@ -15,10 +23,10 @@ export class KernelAdapter {
|
|||||||
model = {
|
model = {
|
||||||
/**
|
/**
|
||||||
* 添加模型到场景
|
* 添加模型到场景
|
||||||
* @param modelUrl 模型URL路径
|
* @param modelInput 模型配置对象
|
||||||
*/
|
*/
|
||||||
add: async (name:string,modelUrl: string): Promise<void> => {
|
add: async (modelInput: ModelInput): Promise<void> => {
|
||||||
await this.mainApp.appModel.add(name,modelUrl);
|
await this.mainApp.appModel.add(modelInput);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 销毁指定模型
|
* 销毁指定模型
|
||||||
@ -37,11 +45,18 @@ export class KernelAdapter {
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 替换模型
|
* 替换模型
|
||||||
* @param modelName 要替换的模型名称
|
* @param modelInput 模型配置对象
|
||||||
* @param newModelUrl 新模型的URL路径
|
|
||||||
*/
|
*/
|
||||||
replace: async (modelName: string, newModelUrl: string): Promise<void> => {
|
replace: async (modelInput: ModelInput): Promise<void> => {
|
||||||
await this.mainApp.appModel.replaceModel(modelName, newModelUrl);
|
await this.mainApp.appModel.replaceModel(modelInput);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 根据网格查找模型名称
|
||||||
|
* @param mesh 网格对象
|
||||||
|
* @returns 模型名称,未找到返回 undefined
|
||||||
|
*/
|
||||||
|
findModelNameByMesh: (mesh: any): string | undefined => {
|
||||||
|
return this.mainApp.appModel.findModelNameByMesh(mesh);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,7 +147,83 @@ export class KernelAdapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 模型变换管理 */
|
||||||
|
transform = {
|
||||||
|
/**
|
||||||
|
* 设置模型旋转
|
||||||
|
* @param options 旋转配置 { modelId: string, vector3: { x, y, z }, useDegrees?: boolean }
|
||||||
|
* @example
|
||||||
|
* // 使用角度(默认)
|
||||||
|
* kernel.transform.rotation({ modelId: "model1", vector3: { x: 0, y: 90, z: 0 } });
|
||||||
|
* // 使用弧度
|
||||||
|
* kernel.transform.rotation({ modelId: "model1", vector3: { x: 0, y: Math.PI / 2, z: 0 }, useDegrees: false });
|
||||||
|
*/
|
||||||
|
rotation: (options: { modelId: string; vector3: { x: number; y: number; z: number }; useDegrees?: boolean }): void => {
|
||||||
|
this.mainApp.appModel.setRotation(options.modelId, options.vector3, options.useDegrees !== false);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 累加模型旋转
|
||||||
|
* @param options 旋转配置 { modelId: string, vector3: { x, y, z }, useDegrees?: boolean }
|
||||||
|
* @example
|
||||||
|
* // 使用角度(默认)
|
||||||
|
* kernel.transform.addRotation({ modelId: "model1", vector3: { x: 0, y: 90, z: 0 } });
|
||||||
|
*/
|
||||||
|
addRotation: (options: { modelId: string; vector3: { x: number; y: number; z: number }; useDegrees?: boolean }): void => {
|
||||||
|
this.mainApp.appModel.addRotation(options.modelId, options.vector3, options.useDegrees !== false);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 设置模型位置
|
||||||
|
* @param options 位置配置 { modelId: string, vector3: { x, y, z } }
|
||||||
|
*/
|
||||||
|
position: (options: { modelId: string; vector3: { x: number; y: number; z: number } }): void => {
|
||||||
|
this.mainApp.appModel.setPosition(options.modelId, options.vector3);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 设置模型缩放
|
||||||
|
* @param options 缩放配置 { modelId: string, vector3: { x, y, z } }
|
||||||
|
*/
|
||||||
|
scale: (options: { modelId: string; vector3: { x: number; y: number; z: number } }): void => {
|
||||||
|
this.mainApp.appModel.setScale(options.modelId, options.vector3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** 调试工具 */
|
/** 调试工具 */
|
||||||
|
selection = {
|
||||||
|
enable: (): void => {
|
||||||
|
this.mainApp.appSelectionOutline.setEnabled(true);
|
||||||
|
},
|
||||||
|
disable: (): void => {
|
||||||
|
this.mainApp.appSelectionOutline.setEnabled(false);
|
||||||
|
},
|
||||||
|
clear: (): void => {
|
||||||
|
this.mainApp.appSelectionOutline.clear();
|
||||||
|
},
|
||||||
|
style: (options: { color?: string; width?: number; thickness?: number; occlusionStrength?: number; occlusionThreshold?: number }): void => {
|
||||||
|
this.mainApp.appSelectionOutline.setStyle(options);
|
||||||
|
},
|
||||||
|
get: (): any[] => {
|
||||||
|
return this.mainApp.appSelectionOutline.getSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gizmo = {
|
||||||
|
enablePosition: (): void => {
|
||||||
|
this.mainApp.appPositionGizmo.setEnabled(true);
|
||||||
|
},
|
||||||
|
disablePosition: (): void => {
|
||||||
|
this.mainApp.appPositionGizmo.setEnabled(false);
|
||||||
|
},
|
||||||
|
togglePosition: (): void => {
|
||||||
|
this.mainApp.appPositionGizmo.toggle();
|
||||||
|
},
|
||||||
|
detach: (): void => {
|
||||||
|
this.mainApp.appPositionGizmo.detach();
|
||||||
|
},
|
||||||
|
isPositionEnabled: (): boolean => {
|
||||||
|
return this.mainApp.appPositionGizmo.isEnabled();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
debug = {
|
debug = {
|
||||||
/** 列出当前场景网格名称 */
|
/** 列出当前场景网格名称 */
|
||||||
listMeshNames: (): string[] => {
|
listMeshNames: (): string[] => {
|
||||||
|
|||||||
16
src/main.ts
16
src/main.ts
@ -23,6 +23,18 @@ type InitParams = {
|
|||||||
rotationY?: number;
|
rotationY?: number;
|
||||||
background?: boolean;
|
background?: boolean;
|
||||||
};
|
};
|
||||||
|
gizmo?: {
|
||||||
|
position?: boolean;
|
||||||
|
rotation?: boolean;
|
||||||
|
scale?: boolean;
|
||||||
|
};
|
||||||
|
outline?: {
|
||||||
|
enable?: boolean;
|
||||||
|
color?: string;
|
||||||
|
thickness?: number;
|
||||||
|
occlusionStrength?: number;
|
||||||
|
occlusionThreshold?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let mainApp: MainApp | null = null;
|
let mainApp: MainApp | null = null;
|
||||||
@ -53,7 +65,9 @@ const kernel = {
|
|||||||
mainApp.loadAConfig({
|
mainApp.loadAConfig({
|
||||||
container,
|
container,
|
||||||
modelUrlList: params.modelUrlList || [],
|
modelUrlList: params.modelUrlList || [],
|
||||||
env: params.env
|
env: params.env,
|
||||||
|
gizmo: params.gizmo,
|
||||||
|
outline: params.outline,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mainApp.Awake();
|
await mainApp.Awake();
|
||||||
|
|||||||
Reference in New Issue
Block a user