diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7dc2eb9..0859e33 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,9 @@ { "permissions": { "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\" {} \\\\;)" ] } } diff --git a/ScreenShot_2026-04-21_103919_342.png b/ScreenShot_2026-04-21_103919_342.png new file mode 100644 index 0000000..f973ffb Binary files /dev/null and b/ScreenShot_2026-04-21_103919_342.png differ diff --git a/ScreenShot_2026-04-21_141406_815.png b/ScreenShot_2026-04-21_141406_815.png new file mode 100644 index 0000000..0bc8bd9 Binary files /dev/null and b/ScreenShot_2026-04-21_141406_815.png differ diff --git a/assets/btn_热点.png b/assets/btn_热点.png new file mode 100644 index 0000000..704efe7 Binary files /dev/null and b/assets/btn_热点.png differ diff --git a/assets/hdr.env b/assets/hdr.env new file mode 100644 index 0000000..8f061ef Binary files /dev/null and b/assets/hdr.env differ diff --git a/assets/model.glb b/assets/model.glb new file mode 100644 index 0000000..1e1a316 Binary files /dev/null and b/assets/model.glb differ diff --git a/index.html b/index.html index 69e033e..f8d0441 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,8 @@ - 3D Model Showcase SDK - TS
+
- - - - + \ No newline at end of file diff --git a/src/babylonjs/AppCamera.ts b/src/babylonjs/AppCamera.ts index cef1aa9..7e70aee 100644 --- a/src/babylonjs/AppCamera.ts +++ b/src/babylonjs/AppCamera.ts @@ -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.attachControl(canvas, true); this.object.minZ = 0.01; // 近裁剪面 - this.object.wheelPrecision =999999; // 滚轮缩放精度 + // this.object.wheelPrecision =999999; // 滚轮缩放精度 this.object.panningSensibility = 0; // 限制垂直角范围,实现上帝视角 diff --git a/src/babylonjs/AppHotspot.ts b/src/babylonjs/AppHotspot.ts new file mode 100644 index 0000000..f96d3de --- /dev/null +++ b/src/babylonjs/AppHotspot.ts @@ -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 = 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) { + // 确保 hotSpot 已初始化 + if (!this.hotSpot) { + this.Awake(); + } + this.initHotSpot(hotSpotList); + } + + initHotSpot(hotSpotList: Array) { + + 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) + } + } + +} diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts index 1f05fc1..8e45935 100644 --- a/src/babylonjs/AppRay.ts +++ b/src/babylonjs/AppRay.ts @@ -71,6 +71,15 @@ class AppRay extends Monobehiver { // 处理单击 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) { EventBridge.modelClick({ meshName: pickInfo.pickedMesh.name, @@ -125,18 +134,7 @@ class AppRay extends Monobehiver { * @param hotspots 热点数据 */ renderHotspots(hotspots: any[]): void { - console.log('Rendering hotspots:', hotspots); - - // 这里需要根据实际的热点渲染逻辑实现 - // 示例实现: - // 1. 清除现有的热点 - // 2. 根据热点数据创建新的热点标记 - // 3. 为热点添加交互事件 - - hotspots.forEach((hotspot, index) => { - console.log(`Rendering hotspot ${index}:`, hotspot); - // 这里需要根据实际的热点数据结构实现 - }); + this.mainApp.appHotspot?.render(hotspots); } } diff --git a/src/babylonjs/GameManager.ts b/src/babylonjs/GameManager.ts index 833dfc9..6d6bb34 100644 --- a/src/babylonjs/GameManager.ts +++ b/src/babylonjs/GameManager.ts @@ -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 { Nullable } from "@babylonjs/core/types"; import { Monobehiver } from '../base/Monobehiver'; @@ -441,10 +441,10 @@ export class GameManager extends Monobehiver { this.rollerDoorGroup!.position.y = next; // 打印每个卷帘门的当前位置 - console.log('Roller door positions:'); - for (const mesh of this.rollerDoorMeshes) { - console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`); - } + // console.log('Roller door positions:'); + // for (const mesh of this.rollerDoorMeshes) { + // console.log(`${mesh.name}: ${mesh.position.y.toFixed(2)}`); + // } }); } @@ -544,8 +544,8 @@ export class GameManager extends Monobehiver { // 创建或获取 group 作为父级 if (!this.rollerDoorGroup) { // 创建一个 AbstractMesh 作为组 - this.rollerDoorGroup = new AbstractMesh('rollerDoorGroup', scene); - // 确保 group 的缩放为 1 + // 使用 TransformNode 代替 AbstractMesh,因为 AbstractMesh 是抽象类无法实例化 + this.rollerDoorGroup = new TransformNode('rollerDoorGroup', scene) as any; // 确保 group 的初始位置为 (0, 0, 0) this.rollerDoorGroup.position.set(0, 0, 0); } diff --git a/src/babylonjs/MainApp.ts b/src/babylonjs/MainApp.ts index d892bb1..7493435 100644 --- a/src/babylonjs/MainApp.ts +++ b/src/babylonjs/MainApp.ts @@ -13,6 +13,7 @@ import { AppConfig } from './AppConfig'; import { AppRay } from './AppRay'; import { GameManager } from './GameManager'; import { EventBridge } from '../event/bridge'; +import { AppHotspot } from './AppHotspot'; /** * 主应用类 - 3D场景的核心控制器 @@ -26,6 +27,7 @@ export class MainApp { appLight: AppLight; appEnv: AppEnv; appRay: AppRay; + appHotspot: AppHotspot; gameManager: GameManager; @@ -37,6 +39,7 @@ export class MainApp { this.appLight = new AppLight(this); this.appEnv = new AppEnv(this); this.appRay = new AppRay(this); + this.appHotspot = new AppHotspot(this); this.gameManager = new GameManager(this); window.addEventListener("resize", () => this.appEngin.handleResize()); @@ -84,5 +87,6 @@ export class MainApp { async dispose(): Promise { this.appModel?.clean(); this.appEnv?.clean(); + this.appHotspot?.clear(); } } diff --git a/src/event/bridge.ts b/src/event/bridge.ts index 5063603..16072f6 100644 --- a/src/event/bridge.ts +++ b/src/event/bridge.ts @@ -4,7 +4,8 @@ import { ModelLoadedPayload, ModelLoadErrorPayload, ModelLoadProgressPayload, - SceneReadyPayload + SceneReadyPayload, + HotspotClickPayload } from './types'; /** @@ -34,6 +35,9 @@ export class EventBridge { static allReady(payload: SceneReadyPayload): Emitter { return emit("all:ready", payload); } + static hotspotClick(payload: HotspotClickPayload): Emitter { + return emit("hotspot:click", payload); + } // Listeners 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 { 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 { return once("scene:ready", callback, context); } diff --git a/src/event/types.ts b/src/event/types.ts index e95cb07..3152994 100644 --- a/src/event/types.ts +++ b/src/event/types.ts @@ -35,3 +35,10 @@ export type ModelClickPayload = { export type SceneReadyPayload = { scene: Scene | null; }; + +export type HotspotClickPayload = { + id: string; + name?: string; + meshName?: string; + payload?: unknown; +}; diff --git a/src/hotspot/HotSpot.ts b/src/hotspot/HotSpot.ts new file mode 100644 index 0000000..6b5eb0d --- /dev/null +++ b/src/hotspot/HotSpot.ts @@ -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 } diff --git a/src/hotspot/HotspotPrams.ts b/src/hotspot/HotspotPrams.ts new file mode 100644 index 0000000..123fcae --- /dev/null +++ b/src/hotspot/HotspotPrams.ts @@ -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 +} diff --git a/src/hotspot/Point.ts b/src/hotspot/Point.ts new file mode 100644 index 0000000..a93d697 --- /dev/null +++ b/src/hotspot/Point.ts @@ -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 | 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) + } + )) + } + } +} diff --git a/src/hotspot/Point_Pool.ts b/src/hotspot/Point_Pool.ts new file mode 100644 index 0000000..6b7a313 --- /dev/null +++ b/src/hotspot/Point_Pool.ts @@ -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 + } + } + } +} diff --git a/src/hotspot/btn_热点.png b/src/hotspot/btn_热点.png new file mode 100644 index 0000000..704efe7 Binary files /dev/null and b/src/hotspot/btn_热点.png differ diff --git a/src/hotspot/index.ts b/src/hotspot/index.ts new file mode 100644 index 0000000..d322c3d --- /dev/null +++ b/src/hotspot/index.ts @@ -0,0 +1,3 @@ +export * from './HotSpot' +export * from './Point' +export * from './HotspotPrams' diff --git a/src/hotspot/style/point.css b/src/hotspot/style/point.css new file mode 100644 index 0000000..fcbf6e9 --- /dev/null +++ b/src/hotspot/style/point.css @@ -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%; +} */ \ No newline at end of file diff --git a/src/kernel/Adapter.ts b/src/kernel/Adapter.ts index 6c9bc57..39ed7f3 100644 --- a/src/kernel/Adapter.ts +++ b/src/kernel/Adapter.ts @@ -1,4 +1,5 @@ -import { MainApp } from '../babylonjs/MainApp'; +import { MainApp } from '../babylonjs/MainApp'; +import type { HotspotInput } from '../types/hotspot'; /** * Kernel 转接器类 - 封装 mainApp 的功能,提供统一�?API 接口 @@ -65,8 +66,8 @@ export class KernelAdapter { * 渲染热点 * @param hotspots 热点数据 */ - render: (hotspots: any[]): void => { - this.mainApp.appRay.renderHotspots(hotspots); + render: (hotspots: HotspotInput[]): void => { + this.mainApp.appHotspot.render(hotspots); } }; /** 调试工具 */ diff --git a/src/types/hotspot.ts b/src/types/hotspot.ts new file mode 100644 index 0000000..8647039 --- /dev/null +++ b/src/types/hotspot.ts @@ -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; +};