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

View File

@ -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\" {} \\\\;)"
] ]
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

BIN
assets/btn_热点.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
assets/hdr.env Normal file

Binary file not shown.

BIN
assets/model.glb Normal file

Binary file not shown.

View File

@ -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.door.toggle({ upY: 28, downY: 0, speed: 12 });
// Y轴剖切只作用于卷帘门网格保留下方剖掉上方
const clipHeight = 28; // 调整这个值找到合适的剖切高度
console.log('设置剖切:', clipHeight);
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
// 验证剖切是否生效
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);
}); });
kernel.on('hotspot:click', (data) => {
console.log('热点被点击:', data);
const { id, name } = data
if (name === "卷帘门") {
kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
// Y轴剖切只作用于卷帘门网格保留下方剖掉上方
const clipHeight = 28; // 调整这个值找到合适的剖切高度
console.log('设置剖切:', clipHeight);
kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
}
// data 包含: { id, name, meshName, payload }
});
</script> </script>
</body> </body>
</html> </html>

View File

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

View File

@ -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);
// 这里需要根据实际的热点数据结构实现
});
} }
} }

View File

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

View File

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

View File

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

View File

@ -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
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%;
} */

View File

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