This commit is contained in:
2026-04-21 14:58:22 +08:00
parent 2f48948e43
commit e7c1611f6b
23 changed files with 748 additions and 49 deletions

98
src/hotspot/HotSpot.ts Normal file
View 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 }

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

3
src/hotspot/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './HotSpot'
export * from './Point'
export * from './HotspotPrams'

157
src/hotspot/style/point.css Normal file
View 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%;
} */