diff --git a/sdk-src/plugins/hotpoint/HotpointPlugin.ts b/sdk-src/plugins/hotpoint/HotpointPlugin.ts new file mode 100644 index 0000000..1e8613b --- /dev/null +++ b/sdk-src/plugins/hotpoint/HotpointPlugin.ts @@ -0,0 +1,171 @@ +import * as THREE from "three"; +import { CSS2DObject, CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; + +import { BaseViewer, CSS2DObjectUtils, log, Hotpoint, matrixAutoUpdate, Plugin, ViewerEvent, Vector2, Vector3 } from "src/core"; + +/** + * Hotpoint plugin manages hotpoints in a viewer. + * It can be used by DxfViewer and BimViewer. + * The hotpoint feature in VRViewer is more complex, that one has nothing to do with this plugin. + * VRViewer is able to use this plugin though. + * - A hotpoint is created and stored by user. User define its html and css. + * - A hotpoint can be added to, and removed from viewer. + * - Caller should set a hotpointId that is unique in the session of current viewer. + * - DxfViewer doesn't maintain the relationship between hotpoint and layout. + */ +export class HotpointPlugin extends Plugin { + protected hotpointRoot?: THREE.Group; + protected css2dRenderer: CSS2DRenderer; + + constructor(viewer: BaseViewer) { + super(viewer, { id: "HotpointPlugin" }); + + // eslint-disable-next-line + this.css2dRenderer = (this.viewer as any).css2dRenderer; + + this.viewer.addEventListener(ViewerEvent.AfterRender, this.onAfterRender); + } + + /** + * @description {en} Adds a hotpoint. + * Caller should set a hotpointId that is unique in the session of current viewer. + * @description {zh} 添加热点。 + * 调用者应该设置一个在当前Viewer会话中唯一的热点id。 + * @param hotpoint + * - {en} hotpoint data. + * - {zh} 热点数据。 + * @example + * ``` typescript + * const hotpoint = { + * hotpointId: "c6ea70a3-ddb0-4dd0-87c8-bd2491936428", + * anchorPosition: [0, 0, 0], + * html: "
hotpoint
", + * visible: true, + * }; + * const plugin = new HotpointPlugin(viewer); + * plugin.add(hotpoint); + * ``` + */ + add(hotpoint: Hotpoint): void { + const exists = this.has(hotpoint.hotpointId); + if (exists) { + log.warn(`[Hotpoint] Hotpoint with id '${hotpoint.hotpointId}' already exist!`); + return; + } + const p = hotpoint.anchorPosition; + const object = CSS2DObjectUtils.createHotpoint(hotpoint.html); + object.position.set(p[0] || 0, p[1] || 0, p[2] || 0); + object.visible = hotpoint.visible !== false; + object.userData.hotpoint = hotpoint; + + if (!this.hotpointRoot) { + this.hotpointRoot = new THREE.Group(); + this.hotpointRoot.matrixAutoUpdate = matrixAutoUpdate; + this.hotpointRoot.matrixWorldAutoUpdate = false; + this.hotpointRoot.name = "HotpointRoot"; + this.viewer.scene?.add(this.hotpointRoot); + } + this.hotpointRoot.add(object); + object.updateWorldMatrix(false, false); + this.viewer.enableRender(); + } + + /** + * @description {en} Removes a hotpoint by given hotpointId. + * @description {zh} 根据热点id删除热点。 + * @param {string} hotpointId + * - {en} hotpoint id. + * - {zh} 热点id。 + * @example + * ``` typescript + * const hotpointId = "c6ea70a3-ddb0-4dd0-87c8-bd2491936428"; + * const plugin = new HotpointPlugin(viewer); + * plugin.remove(hotpointId); + * ``` + */ + remove(hotpointId: string): void { + const objects = this.hotpointRoot?.children || []; + for (let i = 0; i < objects.length; ++i) { + const obj = objects[i]; + if (obj.userData.hotpoint?.hotpointId === hotpointId) { + obj.removeFromParent(); + } + } + } + + /** + * @description {en} Clears all hotpoints. + * @description {zh} 清除所有热点。 + * @example + * ``` typescript + * const plugin = new HotpointPlugin(viewer); + * plugin.clear(); + * ``` + */ + clear(): void { + this.hotpointRoot?.clear(); + } + + /** + * Checks if hotpoint with specific id already exist + * Caller should set a hotpointId that is unique in the session of current DxfViewer. + * @internal + */ + has(hotpointId: string): boolean { + return !!this.findHotpointObject(hotpointId); + } + + /** + * @description {en} Moves a hotpoint. + * @description {zh} 移动热点的位置。 + * @example + * ``` typescript + * const hotpointId = "c6ea70a3-ddb0-4dd0-87c8-bd2491936428"; + * const plugin = new HotpointPlugin(viewer); + * plugin.move(hotpointId, [10, 10, 0]); + * ``` + */ + move(hotpointId: string, position: Vector2 | Vector3): void { + const object = this.findHotpointObject(hotpointId); + if (object) { + const z = (position as Vector3).z || 0; + object.position.set(position.x, position.y, z); + object.updateWorldMatrix(false, false); + this.viewer.enableRender(); + // we probably don't need to update the anchorPosition + } + } + + /** + * @description {en} Hides or show a hotpoint. + * @description {zh} 显示或隐藏一个热点。 + * @example + * ``` typescript + * const hotpointId = "c6ea70a3-ddb0-4dd0-87c8-bd2491936428"; + * const plugin = new HotpointPlugin(viewer); + * plugin.setVisible(hotpointId, false); + * ``` + */ + setVisible(hotpointId: string, visible: boolean): void { + const object = this.findHotpointObject(hotpointId); + if (object) { + object.visible = visible; + } + } + + protected findHotpointObject(hotpointId: string): CSS2DObject | undefined { + const objects = this.hotpointRoot?.children || []; + const object = objects.find((obj) => obj.userData.hotpoint?.hotpointId === hotpointId); + return object as CSS2DObject; + } + + protected onAfterRender = () => { + const scene = this.viewer.scene; + const camera = this.viewer.camera; + if (!scene || !camera || !this.hotpointRoot || this.hotpointRoot.children.length === 0) { + return; + } + // TODO: if css2dRenderer.render() is already called in viewer, then we don't need to call it again. + this.css2dRenderer?.render(scene, camera); + }; +}