1
This commit is contained in:
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(cd /d/VscodeProject/zhengte.babylonjs-sdk && npm run build 2>&1 | head -50)"
|
"Bash(cd /d/VscodeProject/zhengte.babylonjs-sdk && npm run build 2>&1 | head -50)",
|
||||||
|
"Bash(find \"D:\\\\VscodeProject\\\\zhengte.babylonjs-sdk\\\\src\" -name \"*.ts\" -type f -exec grep -l \"class Dictionary\" {} \\\\;)",
|
||||||
|
"Bash(find \"D:\\\\VscodeProject\\\\zhengte.babylonjs-sdk\\\\src\" -name \"*.ts\" -type f -exec grep -l \"class.*Kernel\\\\|export.*kernel\" {} \\\\;)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ScreenShot_2026-04-21_103919_342.png
Normal file
BIN
ScreenShot_2026-04-21_103919_342.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 475 KiB |
BIN
ScreenShot_2026-04-21_141406_815.png
Normal file
BIN
ScreenShot_2026-04-21_141406_815.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 642 KiB |
BIN
assets/btn_热点.png
Normal file
BIN
assets/btn_热点.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/hdr.env
Normal file
BIN
assets/hdr.env
Normal file
Binary file not shown.
BIN
assets/model.glb
Normal file
BIN
assets/model.glb
Normal file
Binary file not shown.
103
index.html
103
index.html
@ -4,7 +4,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>3D Model Showcase SDK - TS</title><style>
|
<title>3D Model Showcase SDK - TS</title>
|
||||||
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -28,12 +29,45 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 进度条样式 */
|
||||||
|
#progress-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-bar {
|
||||||
|
width: 0%;
|
||||||
|
height: 10px;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-text {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<canvas id="renderDom"></canvas>
|
<canvas id="renderDom"></canvas>
|
||||||
|
<div id="progress-container" style="display: none;">
|
||||||
|
<div id="progress-bar"></div>
|
||||||
|
<div id="progress-text">0%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@ -43,8 +77,8 @@
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
container: document.querySelector('#renderDom'),
|
container: document.querySelector('#renderDom'),
|
||||||
modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'],
|
modelUrlList: ['/assets/model.glb'],
|
||||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
env: { envPath: '/assets/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
kernel.init(config);
|
kernel.init(config);
|
||||||
@ -53,16 +87,34 @@
|
|||||||
|
|
||||||
kernel.on('model:load:progress', (data) => {
|
kernel.on('model:load:progress', (data) => {
|
||||||
console.log('模型加载事件', data);
|
console.log('模型加载事件', data);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const progress = data.progress || 0;
|
||||||
|
const progressBar = document.getElementById('progress-bar');
|
||||||
|
const progressText = document.getElementById('progress-text');
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
}
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = `${progress * 100}%`;
|
||||||
|
}
|
||||||
|
if (progressText) {
|
||||||
|
progressText.textContent = `${Math.round(progress * 100)}%`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
kernel.on('model:loaded', (data) => {
|
kernel.on('model:loaded', (data) => {
|
||||||
console.log('模型加载完成', data);
|
console.log('模型加载完成', data);
|
||||||
|
// 隐藏进度条
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
kernel.on('all:ready', (data) => {
|
kernel.on('all:ready', (data) => {
|
||||||
@ -73,36 +125,47 @@
|
|||||||
value: 0.5,
|
value: 0.5,
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
kernel.hotspot.render([
|
||||||
|
{
|
||||||
|
id: "h1",
|
||||||
|
name: "卷帘门",
|
||||||
|
meshName: "Valve_01",
|
||||||
|
icon: "/assets/btn_热点.png",
|
||||||
|
offset: [25, 25, 0],
|
||||||
|
radius: 20,
|
||||||
|
color: "#21c7ff",
|
||||||
|
payload: { type: "valve", code: "A" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
kernel.on('model:click', (data) => {
|
kernel.on('model:click', (data) => {
|
||||||
console.log('模型点击事件', data);
|
console.log('模型点击事件', data);
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
// 切换卷帘门开关
|
// 切换卷帘门开关
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
kernel.on('hotspot:click', (data) => {
|
||||||
|
console.log('热点被点击:', data);
|
||||||
|
const { id, name } = data
|
||||||
|
if (name === "卷帘门") {
|
||||||
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||||
|
|
||||||
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
// Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||||
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||||
console.log('设置剖切:', clipHeight);
|
console.log('设置剖切:', clipHeight);
|
||||||
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||||
|
}
|
||||||
|
|
||||||
// 验证剖切是否生效
|
// data 包含: { id, name, meshName, payload }
|
||||||
setTimeout(() => {
|
|
||||||
const scene = kernel.mainApp?.appScene?.object;
|
|
||||||
console.log('Scene:', scene);
|
|
||||||
console.log('Scene clipPlane:', scene?.clipPlane);
|
|
||||||
console.log('Scene meshes count:', scene?.meshes?.length);
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export class AppCamera extends Monobehiver {
|
|||||||
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
|
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
|
||||||
this.object.attachControl(canvas, true);
|
this.object.attachControl(canvas, true);
|
||||||
this.object.minZ = 0.01; // 近裁剪面
|
this.object.minZ = 0.01; // 近裁剪面
|
||||||
this.object.wheelPrecision =999999; // 滚轮缩放精度
|
// this.object.wheelPrecision =999999; // 滚轮缩放精度
|
||||||
this.object.panningSensibility = 0;
|
this.object.panningSensibility = 0;
|
||||||
|
|
||||||
// 限制垂直角范围,实现上帝视角
|
// 限制垂直角范围,实现上帝视角
|
||||||
|
|||||||
173
src/babylonjs/AppHotspot.ts
Normal file
173
src/babylonjs/AppHotspot.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { Vector3 } from '@babylonjs/core'
|
||||||
|
import { Monobehiver } from '../base/Monobehiver'
|
||||||
|
import { HotSpot, HotspotPrams, Point } from '../hotspot'
|
||||||
|
// import { userSellingPointStore } from '@/stores/zguiy'
|
||||||
|
import type { MainApp } from './MainApp'
|
||||||
|
import { Dictionary } from '../utils/Dictionary'
|
||||||
|
import { EventBridge } from '../event/bridge'
|
||||||
|
|
||||||
|
|
||||||
|
export class AppHotspot extends Monobehiver {
|
||||||
|
hotSpot!: HotSpot
|
||||||
|
sllingPointStore: any
|
||||||
|
//偏移量
|
||||||
|
offset: number = 0.7
|
||||||
|
yundong: boolean = false
|
||||||
|
|
||||||
|
|
||||||
|
hotspotDic: Dictionary<HotSpot> = new Dictionary()
|
||||||
|
|
||||||
|
constructor(mainApp: MainApp) {
|
||||||
|
super(mainApp)
|
||||||
|
// this.sllingPointStore = userSellingPointStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
Awake() {
|
||||||
|
|
||||||
|
|
||||||
|
const hotspot = new HotSpot(this.mainApp)
|
||||||
|
hotspot.Awake()
|
||||||
|
this.hotSpot = hotspot;
|
||||||
|
|
||||||
|
// 注意:需要从外部传入热点列表,或者从配置中读取
|
||||||
|
// this.initHotSpot(hotSpotList)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render(hotSpotList: Array<any>) {
|
||||||
|
// 确保 hotSpot 已初始化
|
||||||
|
if (!this.hotSpot) {
|
||||||
|
this.Awake();
|
||||||
|
}
|
||||||
|
this.initHotSpot(hotSpotList);
|
||||||
|
}
|
||||||
|
|
||||||
|
initHotSpot(hotSpotList: Array<any>) {
|
||||||
|
|
||||||
|
hotSpotList.forEach((hotspot: any) => {
|
||||||
|
|
||||||
|
this.createHotspot(hotspot)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createHotspot(hotspot: any) {
|
||||||
|
|
||||||
|
// 检查必要的数据
|
||||||
|
if (!hotspot) {
|
||||||
|
console.warn('热点数据为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('热点原始数据:', hotspot);
|
||||||
|
|
||||||
|
let position: Vector3;
|
||||||
|
|
||||||
|
// 使用 offset 作为 position
|
||||||
|
if (hotspot.offset) {
|
||||||
|
if (Array.isArray(hotspot.offset)) {
|
||||||
|
console.log('offset 数组:', hotspot.offset);
|
||||||
|
position = new Vector3(
|
||||||
|
hotspot.offset[0] ?? 0,
|
||||||
|
hotspot.offset[1] ?? 0,
|
||||||
|
hotspot.offset[2] ?? 0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
position = new Vector3(
|
||||||
|
hotspot.offset.x ?? 0,
|
||||||
|
hotspot.offset.y ?? 0,
|
||||||
|
hotspot.offset.z ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (hotspot.position) {
|
||||||
|
// 兼容 position 字段
|
||||||
|
if (Array.isArray(hotspot.position)) {
|
||||||
|
position = new Vector3(
|
||||||
|
hotspot.position[0] ?? 0,
|
||||||
|
hotspot.position[1] ?? 0,
|
||||||
|
hotspot.position[2] ?? 0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
position = new Vector3(
|
||||||
|
hotspot.position.x ?? 0,
|
||||||
|
hotspot.position.y ?? 0,
|
||||||
|
hotspot.position.z ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('热点数据缺少 position 或 offset 字段:', hotspot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('创建热点:', hotspot.name, 'position:', position, 'x:', position.x, 'y:', position.y, 'z:', position.z);
|
||||||
|
|
||||||
|
const disposition = Vector3.Zero();
|
||||||
|
|
||||||
|
this.hotSpot.Point_Event(
|
||||||
|
new HotspotPrams(
|
||||||
|
position,
|
||||||
|
disposition,
|
||||||
|
() => {
|
||||||
|
},
|
||||||
|
async (p: Point) => {
|
||||||
|
console.log('热点被点击:', hotspot.name, hotspot.payload)
|
||||||
|
// 触发热点点击事件
|
||||||
|
EventBridge.hotspotClick({
|
||||||
|
id: hotspot.id,
|
||||||
|
name: hotspot.name,
|
||||||
|
meshName: hotspot.meshName,
|
||||||
|
payload: hotspot.payload
|
||||||
|
})
|
||||||
|
},
|
||||||
|
hotspot.icon,
|
||||||
|
hotspot.radius
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
// 首先隐藏所有热点
|
||||||
|
this.visible(false);
|
||||||
|
|
||||||
|
// 如果存在热点池
|
||||||
|
if (this.hotSpot && this.hotSpot._point_Pool && this.hotSpot._point_Pool.points) {
|
||||||
|
// 遍历所有热点
|
||||||
|
for (let i = 0; i < this.hotSpot._point_Pool.points.length; i++) {
|
||||||
|
const point = this.hotSpot._point_Pool.points[i];
|
||||||
|
|
||||||
|
// 清除事件监听器
|
||||||
|
if (point.img && point.onCallBack) {
|
||||||
|
point.img.removeEventListener('mousedown', point.onCallBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从DOM中移除注释元素
|
||||||
|
if (point.annotation && point.annotation.parentNode) {
|
||||||
|
point.annotation.parentNode.removeChild(point.annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放sprite资源
|
||||||
|
if (point.sprite) {
|
||||||
|
point.sprite.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空热点池
|
||||||
|
this.hotSpot._point_Pool.points = [];
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('热点资源已释放');
|
||||||
|
}
|
||||||
|
|
||||||
|
visible(visible: boolean) {
|
||||||
|
console.log(visible);
|
||||||
|
|
||||||
|
if (this.hotSpot) {
|
||||||
|
this.hotSpot.Enable_All(visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -71,6 +71,15 @@ class AppRay extends Monobehiver {
|
|||||||
|
|
||||||
// 处理单击
|
// 处理单击
|
||||||
handleSingleClick(evt: IPointerEvent, pickInfo: PickingInfo | null) {
|
handleSingleClick(evt: IPointerEvent, pickInfo: PickingInfo | null) {
|
||||||
|
// 先尝试热点(mesh 热点 / sprite 热点)
|
||||||
|
// if (pickInfo && pickInfo.pickedMesh) {
|
||||||
|
// const isHotspotClick = this.mainApp.appHotspot?.handlePick(pickInfo.pickedMesh);
|
||||||
|
// if (isHotspotClick) return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const isSpriteHotspotClick = this.mainApp.appHotspot?.handleSpritePick();
|
||||||
|
// if (isSpriteHotspotClick) return;
|
||||||
|
|
||||||
if (pickInfo && pickInfo.pickedMesh) {
|
if (pickInfo && pickInfo.pickedMesh) {
|
||||||
EventBridge.modelClick({
|
EventBridge.modelClick({
|
||||||
meshName: pickInfo.pickedMesh.name,
|
meshName: pickInfo.pickedMesh.name,
|
||||||
@ -125,18 +134,7 @@ class AppRay extends Monobehiver {
|
|||||||
* @param hotspots 热点数据
|
* @param hotspots 热点数据
|
||||||
*/
|
*/
|
||||||
renderHotspots(hotspots: any[]): void {
|
renderHotspots(hotspots: any[]): void {
|
||||||
console.log('Rendering hotspots:', hotspots);
|
this.mainApp.appHotspot?.render(hotspots);
|
||||||
|
|
||||||
// 这里需要根据实际的热点渲染逻辑实现
|
|
||||||
// 示例实现:
|
|
||||||
// 1. 清除现有的热点
|
|
||||||
// 2. 根据热点数据创建新的热点标记
|
|
||||||
// 3. 为热点添加交互事件
|
|
||||||
|
|
||||||
hotspots.forEach((hotspot, index) => {
|
|
||||||
console.log(`Rendering hotspot ${index}:`, hotspot);
|
|
||||||
// 这里需要根据实际的热点数据结构实现
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3 } from "@babylonjs/core";
|
import { Mesh, PBRMaterial, Texture, AbstractMesh, Plane, Vector3, Scene, Color3, TransformNode } from "@babylonjs/core";
|
||||||
import { Observer } from "@babylonjs/core/Misc/observable";
|
import { Observer } from "@babylonjs/core/Misc/observable";
|
||||||
import { Nullable } from "@babylonjs/core/types";
|
import { Nullable } from "@babylonjs/core/types";
|
||||||
import { Monobehiver } from '../base/Monobehiver';
|
import { Monobehiver } from '../base/Monobehiver';
|
||||||
@ -441,10 +441,10 @@ export class GameManager extends Monobehiver {
|
|||||||
this.rollerDoorGroup!.position.y = next;
|
this.rollerDoorGroup!.position.y = next;
|
||||||
|
|
||||||
// 打印每个卷帘门的当前位置
|
// 打印每个卷帘门的当前位置
|
||||||
console.log('Roller door positions:');
|
// console.log('Roller door positions:');
|
||||||
for (const mesh of this.rollerDoorMeshes) {
|
// for (const mesh of this.rollerDoorMeshes) {
|
||||||
console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
// console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`);
|
||||||
}
|
// }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,8 +544,8 @@ export class GameManager extends Monobehiver {
|
|||||||
// 创建或获取 group 作为父级
|
// 创建或获取 group 作为父级
|
||||||
if (!this.rollerDoorGroup) {
|
if (!this.rollerDoorGroup) {
|
||||||
// 创建一个 AbstractMesh 作为组
|
// 创建一个 AbstractMesh 作为组
|
||||||
this.rollerDoorGroup = new AbstractMesh('rollerDoorGroup', scene);
|
// 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化
|
||||||
// 确保 group 的缩放为 1
|
this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any;
|
||||||
// 确保 group 的初始位置为 (0, 0, 0)
|
// 确保 group 的初始位置为 (0, 0, 0)
|
||||||
this.rollerDoorGroup.position.set(0, 0, 0);
|
this.rollerDoorGroup.position.set(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { AppConfig } from './AppConfig';
|
|||||||
import { AppRay } from './AppRay';
|
import { AppRay } from './AppRay';
|
||||||
import { GameManager } from './GameManager';
|
import { GameManager } from './GameManager';
|
||||||
import { EventBridge } from '../event/bridge';
|
import { EventBridge } from '../event/bridge';
|
||||||
|
import { AppHotspot } from './AppHotspot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主应用类 - 3D场景的核心控制器
|
* 主应用类 - 3D场景的核心控制器
|
||||||
@ -26,6 +27,7 @@ export class MainApp {
|
|||||||
appLight: AppLight;
|
appLight: AppLight;
|
||||||
appEnv: AppEnv;
|
appEnv: AppEnv;
|
||||||
appRay: AppRay;
|
appRay: AppRay;
|
||||||
|
appHotspot: AppHotspot;
|
||||||
gameManager: GameManager;
|
gameManager: GameManager;
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +39,7 @@ export class MainApp {
|
|||||||
this.appLight = new AppLight(this);
|
this.appLight = new AppLight(this);
|
||||||
this.appEnv = new AppEnv(this);
|
this.appEnv = new AppEnv(this);
|
||||||
this.appRay = new AppRay(this);
|
this.appRay = new AppRay(this);
|
||||||
|
this.appHotspot = new AppHotspot(this);
|
||||||
this.gameManager = new GameManager(this);
|
this.gameManager = new GameManager(this);
|
||||||
|
|
||||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||||
@ -84,5 +87,6 @@ export class MainApp {
|
|||||||
async dispose(): Promise<void> {
|
async dispose(): Promise<void> {
|
||||||
this.appModel?.clean();
|
this.appModel?.clean();
|
||||||
this.appEnv?.clean();
|
this.appEnv?.clean();
|
||||||
|
this.appHotspot?.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import {
|
|||||||
ModelLoadedPayload,
|
ModelLoadedPayload,
|
||||||
ModelLoadErrorPayload,
|
ModelLoadErrorPayload,
|
||||||
ModelLoadProgressPayload,
|
ModelLoadProgressPayload,
|
||||||
SceneReadyPayload
|
SceneReadyPayload,
|
||||||
|
HotspotClickPayload
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +35,9 @@ export class EventBridge {
|
|||||||
static allReady(payload: SceneReadyPayload): Emitter {
|
static allReady(payload: SceneReadyPayload): Emitter {
|
||||||
return emit("all:ready", payload);
|
return emit("all:ready", payload);
|
||||||
}
|
}
|
||||||
|
static hotspotClick(payload: HotspotClickPayload): Emitter {
|
||||||
|
return emit("hotspot:click", payload);
|
||||||
|
}
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
static onModelLoadProgress(callback: (payload: ModelLoadProgressPayload) => void, context?: unknown): Emitter {
|
static onModelLoadProgress(callback: (payload: ModelLoadProgressPayload) => void, context?: unknown): Emitter {
|
||||||
@ -58,6 +62,9 @@ export class EventBridge {
|
|||||||
static onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
static onAllReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
||||||
return on("all:ready", callback, context);
|
return on("all:ready", callback, context);
|
||||||
}
|
}
|
||||||
|
static onHotspotClick(callback: (payload: HotspotClickPayload) => void, context?: unknown): Emitter {
|
||||||
|
return on("hotspot:click", callback, context);
|
||||||
|
}
|
||||||
static onceSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
static onceSceneReady(callback: (payload: SceneReadyPayload) => void, context?: unknown): Emitter {
|
||||||
return once("scene:ready", callback, context);
|
return once("scene:ready", callback, context);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,3 +35,10 @@ export type ModelClickPayload = {
|
|||||||
export type SceneReadyPayload = {
|
export type SceneReadyPayload = {
|
||||||
scene: Scene | null;
|
scene: Scene | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HotspotClickPayload = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
meshName?: string;
|
||||||
|
payload?: unknown;
|
||||||
|
};
|
||||||
|
|||||||
98
src/hotspot/HotSpot.ts
Normal file
98
src/hotspot/HotSpot.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { Point_Pool } from './Point_Pool'
|
||||||
|
import { Point } from './Point'
|
||||||
|
import {
|
||||||
|
AbstractMesh,
|
||||||
|
ArcRotateCamera,
|
||||||
|
Engine,
|
||||||
|
Matrix,
|
||||||
|
Scene,
|
||||||
|
Vector3,
|
||||||
|
Texture,
|
||||||
|
StandardMaterial,
|
||||||
|
MeshBuilder,
|
||||||
|
TransformNode
|
||||||
|
} from '@babylonjs/core'
|
||||||
|
import { HotspotPrams } from './HotspotPrams'
|
||||||
|
import type { MainApp } from '../babylonjs/MainApp'
|
||||||
|
|
||||||
|
class HotSpot {
|
||||||
|
_point_Pool!: Point_Pool
|
||||||
|
body!: HTMLElement
|
||||||
|
_camera!: ArcRotateCamera
|
||||||
|
mainApp!: MainApp
|
||||||
|
hotspotTexture!: Texture
|
||||||
|
hotspotMaterial!: StandardMaterial
|
||||||
|
hotspotContainer!: TransformNode
|
||||||
|
|
||||||
|
vector!: Vector3
|
||||||
|
halfW!: number
|
||||||
|
halfH!: number
|
||||||
|
annotation!: HTMLElement
|
||||||
|
modedl!: AbstractMesh
|
||||||
|
|
||||||
|
|
||||||
|
constructor(mainAPP: MainApp) {
|
||||||
|
this.mainApp = mainAPP
|
||||||
|
}
|
||||||
|
|
||||||
|
Awake() {
|
||||||
|
this._camera = this.mainApp.appCamera.object
|
||||||
|
this._point_Pool = new Point_Pool()
|
||||||
|
|
||||||
|
// 创建热点容器
|
||||||
|
this.hotspotContainer = new TransformNode('hotspotContainer', this.mainApp.appScene.object)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//创建圆点并且生成事件 类型
|
||||||
|
Point_Event(prams: HotspotPrams) {
|
||||||
|
const iconPath = prams.icon
|
||||||
|
|
||||||
|
// 为每个热点创建独立的材质
|
||||||
|
const texture = new Texture(iconPath, this.mainApp.appScene.object)
|
||||||
|
texture.hasAlpha = true
|
||||||
|
texture.getAlphaFromRGB = false
|
||||||
|
|
||||||
|
|
||||||
|
const material = new StandardMaterial(`hotspotMaterial_${Math.random()}`, this.mainApp.appScene.object)
|
||||||
|
material.diffuseTexture = texture
|
||||||
|
material.emissiveTexture = texture
|
||||||
|
material.opacityTexture = texture
|
||||||
|
material.useAlphaFromDiffuseTexture = true
|
||||||
|
material.transparencyMode = 2 // ALPHABLEND 模式
|
||||||
|
material.disableLighting = true
|
||||||
|
material.backFaceCulling = false
|
||||||
|
|
||||||
|
// 检查纹理是否已加载
|
||||||
|
if (texture.isReady()) {
|
||||||
|
// 纹理已准备好,立即创建热点
|
||||||
|
this.createPointPlane(prams, material)
|
||||||
|
} else {
|
||||||
|
// 纹理未准备好,等待加载完成
|
||||||
|
texture.onLoadObservable.addOnce(() => {
|
||||||
|
this.createPointPlane(prams, material)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建点平面的具体实现
|
||||||
|
createPointPlane(prams: HotspotPrams, material: StandardMaterial) {
|
||||||
|
let { position, disposition, onload, onCallBack } = prams
|
||||||
|
let _point = new Point(material, this.hotspotContainer, this.mainApp.appScene.object)
|
||||||
|
_point.init(position, disposition, onload, onCallBack, prams.radius)
|
||||||
|
|
||||||
|
// 将热点添加到热点池中
|
||||||
|
this._point_Pool.Add_point(_point)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Enable_All(visible: boolean) {
|
||||||
|
if (this._point_Pool) {
|
||||||
|
this._point_Pool.Enable_All(visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HotSpot }
|
||||||
24
src/hotspot/HotspotPrams.ts
Normal file
24
src/hotspot/HotspotPrams.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Vector3 } from '@babylonjs/core'
|
||||||
|
export class HotspotPrams {
|
||||||
|
constructor(
|
||||||
|
position: Vector3,
|
||||||
|
disposition: Vector3,
|
||||||
|
onload: Function,
|
||||||
|
onCallBack: Function,
|
||||||
|
icon?: string,
|
||||||
|
radius?: number
|
||||||
|
) {
|
||||||
|
this.position = position
|
||||||
|
this.disposition = disposition
|
||||||
|
this.onload = onload
|
||||||
|
this.onCallBack = onCallBack
|
||||||
|
this.icon = icon
|
||||||
|
this.radius = radius
|
||||||
|
}
|
||||||
|
position!: Vector3
|
||||||
|
disposition!: Vector3
|
||||||
|
onload!: Function
|
||||||
|
onCallBack!: Function
|
||||||
|
icon?: string
|
||||||
|
radius?: number
|
||||||
|
}
|
||||||
105
src/hotspot/Point.ts
Normal file
105
src/hotspot/Point.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
Vector3,
|
||||||
|
ActionManager,
|
||||||
|
ExecuteCodeAction,
|
||||||
|
StandardMaterial,
|
||||||
|
MeshBuilder,
|
||||||
|
Mesh,
|
||||||
|
TransformNode,
|
||||||
|
Scene,
|
||||||
|
Ray,
|
||||||
|
Observer
|
||||||
|
} from '@babylonjs/core'
|
||||||
|
|
||||||
|
export class Point {
|
||||||
|
annotation!: HTMLElement
|
||||||
|
showBox!: HTMLElement
|
||||||
|
position!: Vector3
|
||||||
|
disposition!: Vector3
|
||||||
|
onload!: Function
|
||||||
|
onCallBack!: Function
|
||||||
|
offCallBack!: Function
|
||||||
|
isClick!: boolean
|
||||||
|
img!: any
|
||||||
|
plane!: Mesh
|
||||||
|
spriteBehindObject!: boolean
|
||||||
|
hotspotMaterial!: StandardMaterial
|
||||||
|
hotspotContainer!: TransformNode
|
||||||
|
scene!: Scene
|
||||||
|
occlusionObserver!: Observer<Scene> | null
|
||||||
|
|
||||||
|
constructor(hotspotMaterial: StandardMaterial, hotspotContainer: TransformNode, scene: Scene) {
|
||||||
|
this.hotspotMaterial = hotspotMaterial
|
||||||
|
this.hotspotContainer = hotspotContainer
|
||||||
|
this.scene = scene
|
||||||
|
this.occlusionObserver = null
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
position: Vector3,
|
||||||
|
disposition: Vector3,
|
||||||
|
onload: Function,
|
||||||
|
onCallBack: Function,
|
||||||
|
radius?: number
|
||||||
|
) {
|
||||||
|
this.position = position
|
||||||
|
this.disposition = disposition
|
||||||
|
this.onCallBack = onCallBack
|
||||||
|
this.onload = onload
|
||||||
|
|
||||||
|
|
||||||
|
this.Create_plane(radius)
|
||||||
|
this.setupEvents()
|
||||||
|
//this.Create_annotation(onload, onCallBack)
|
||||||
|
//this.isClick = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Create_plane(radius?: number) {
|
||||||
|
// 创建一个平面作为热点
|
||||||
|
this.plane = MeshBuilder.CreatePlane(
|
||||||
|
Math.random().toString(36).slice(-6),
|
||||||
|
{
|
||||||
|
size: radius ? radius / 10 : 0.14, // 热点大小,如果传入 radius 则缩放
|
||||||
|
sideOrientation: Mesh.DOUBLESIDE
|
||||||
|
},
|
||||||
|
this.scene
|
||||||
|
)
|
||||||
|
|
||||||
|
// 设置热点位置
|
||||||
|
this.plane.position.copyFrom(this.position)
|
||||||
|
|
||||||
|
// 应用材质
|
||||||
|
this.plane.material = this.hotspotMaterial
|
||||||
|
|
||||||
|
// 启用深度测试,让热点被模型遮挡时不显示
|
||||||
|
if (this.plane.material) {
|
||||||
|
this.plane.material.disableDepthWrite = false
|
||||||
|
this.plane.material.needDepthPrePass = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置为公告牌模式,让热点始终面向摄像机
|
||||||
|
this.plane.billboardMode = Mesh.BILLBOARDMODE_ALL
|
||||||
|
|
||||||
|
// 设置父节点为热点容器
|
||||||
|
this.plane.parent = this.hotspotContainer
|
||||||
|
|
||||||
|
// 确保热点可见和可交互
|
||||||
|
this.plane.isVisible = true
|
||||||
|
this.plane.isPickable = true
|
||||||
|
this.plane.renderingGroupId = 1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEvents() {
|
||||||
|
if (this.plane && this.onCallBack) {
|
||||||
|
this.plane.actionManager = new ActionManager(this.scene)
|
||||||
|
this.plane.actionManager.registerAction(new ExecuteCodeAction(
|
||||||
|
ActionManager.OnPickTrigger,
|
||||||
|
() => {
|
||||||
|
console.log('热点被点击:', this.plane.name)
|
||||||
|
this.onCallBack(this)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/hotspot/Point_Pool.ts
Normal file
20
src/hotspot/Point_Pool.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Point } from './Point'
|
||||||
|
|
||||||
|
export class Point_Pool {
|
||||||
|
points: Point[]
|
||||||
|
constructor() {
|
||||||
|
this.points = new Array()
|
||||||
|
}
|
||||||
|
|
||||||
|
Add_point(point_Class: Point) {
|
||||||
|
this.points.push(point_Class)
|
||||||
|
}
|
||||||
|
|
||||||
|
Enable_All(visible: boolean) {
|
||||||
|
for (let i = 0, item; (item = this.points[i++]); ) {
|
||||||
|
if (item.plane) {
|
||||||
|
item.plane.isVisible = visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/hotspot/btn_热点.png
Normal file
BIN
src/hotspot/btn_热点.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
3
src/hotspot/index.ts
Normal file
3
src/hotspot/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './HotSpot'
|
||||||
|
export * from './Point'
|
||||||
|
export * from './HotspotPrams'
|
||||||
157
src/hotspot/style/point.css
Normal file
157
src/hotspot/style/point.css
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/* .canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-top: -10px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 10%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.line_Right {
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
left: 55px;
|
||||||
|
z-index: 0;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-top: -30px;
|
||||||
|
width: 241px;
|
||||||
|
height: 104px;
|
||||||
|
border-radius: 10%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line_Left {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: -210px;
|
||||||
|
z-index: 0;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-top: -30px;
|
||||||
|
width: 241px;
|
||||||
|
height: 104px;
|
||||||
|
border-radius: 10%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
transform-origin: 100% 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ShowBox_left {
|
||||||
|
position: absolute;
|
||||||
|
top: 120px;
|
||||||
|
left: -55px;
|
||||||
|
z-index: 1;
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-top: -10px;
|
||||||
|
width: 70px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 10%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 50px;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
background-size: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ShowBox_right {
|
||||||
|
position: absolute;
|
||||||
|
top: 120px;
|
||||||
|
left: 240px;
|
||||||
|
z-index: 1;
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-top: -10px;
|
||||||
|
width: 70px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 10%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 50px;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
background-size: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after {
|
||||||
|
content: attr(Text);
|
||||||
|
position: absolute;
|
||||||
|
top: -110px;
|
||||||
|
left: 50px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: .5em;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(0, 0, 0, 1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#number {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linimg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointimg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: shrink 1s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes shrink {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointimg:hover {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChangeShowBox {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 50px;
|
||||||
|
z-index: 1;
|
||||||
|
margin-left: -20px;
|
||||||
|
margin-top: -60px;
|
||||||
|
width: 150px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 10%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
background-size: 100%;
|
||||||
|
} */
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { MainApp } from '../babylonjs/MainApp';
|
import { MainApp } from '../babylonjs/MainApp';
|
||||||
|
import type { HotspotInput } from '../types/hotspot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kernel 转接器类 - 封装 mainApp 的功能,提供统一<E7BB9F>?API 接口
|
* Kernel 转接器类 - 封装 mainApp 的功能,提供统一<E7BB9F>?API 接口
|
||||||
@ -65,8 +66,8 @@ export class KernelAdapter {
|
|||||||
* 渲染热点
|
* 渲染热点
|
||||||
* @param hotspots 热点数据
|
* @param hotspots 热点数据
|
||||||
*/
|
*/
|
||||||
render: (hotspots: any[]): void => {
|
render: (hotspots: HotspotInput[]): void => {
|
||||||
this.mainApp.appRay.renderHotspots(hotspots);
|
this.mainApp.appHotspot.render(hotspots);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/** 调试工具 */
|
/** 调试工具 */
|
||||||
|
|||||||
37
src/types/hotspot.ts
Normal file
37
src/types/hotspot.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { Mesh, Sprite, TransformNode } from "@babylonjs/core";
|
||||||
|
|
||||||
|
export type HotspotVectorInput = [number, number, number] | { x: number; y: number; z: number };
|
||||||
|
|
||||||
|
export type HotspotColorInput = string | { r: number; g: number; b: number };
|
||||||
|
|
||||||
|
export type HotspotInput = {
|
||||||
|
/** 热点唯一 id,不传会自动生成 */
|
||||||
|
id?: string;
|
||||||
|
/** 热点显示名 */
|
||||||
|
name?: string;
|
||||||
|
/** 绑定到某个 mesh(随模型一起移动) */
|
||||||
|
meshName?: string;
|
||||||
|
/** 世界坐标位置(未绑定 mesh 时生效) */
|
||||||
|
position?: HotspotVectorInput;
|
||||||
|
/** 相对锚点偏移(绑定 mesh 和世界坐标两种模式都可用) */
|
||||||
|
offset?: HotspotVectorInput;
|
||||||
|
/** 半径 */
|
||||||
|
radius?: number;
|
||||||
|
/** 热点图标(URL 或相对 public 路径),不传使用默认图标 */
|
||||||
|
icon?: string;
|
||||||
|
/** 颜色:十六进制或 rgb 对象 */
|
||||||
|
color?: HotspotColorInput;
|
||||||
|
/** 透明度 */
|
||||||
|
alpha?: number;
|
||||||
|
/** 透传业务数据 */
|
||||||
|
payload?: unknown;
|
||||||
|
/** 是否启用 */
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HotspotRuntime = {
|
||||||
|
id: string;
|
||||||
|
input: HotspotInput;
|
||||||
|
marker: Mesh | Sprite;
|
||||||
|
anchor: TransformNode;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user