This commit is contained in:
2026-04-30 14:46:01 +08:00
parent 604dcdf3fb
commit 4207fcf7c2
15 changed files with 1038 additions and 87 deletions

216
API_USAGE_EXAMPLE.md Normal file
View 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 = 原始大小)
- 可以设置不同轴向的缩放比例

View File

@ -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) {

View File

@ -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
View File

@ -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
View File

@ -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
}, },

View File

@ -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"
}, },

View File

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

View File

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

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

View File

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

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

View File

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

View File

@ -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 = {

View File

@ -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[] => {

View File

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