|
| 1 | +import * as THREE from "three"; |
| 2 | +import { CSS2DObject, CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; |
| 3 | + |
| 4 | +import { BaseViewer, CSS2DObjectUtils, log, Hotpoint, matrixAutoUpdate, Plugin, ViewerEvent, Vector2, Vector3 } from "src/core"; |
| 5 | + |
| 6 | +/** |
| 7 | + * Hotpoint plugin manages hotpoints in a viewer. |
| 8 | + * It can be used by DxfViewer and BimViewer. |
| 9 | + * The hotpoint feature in VRViewer is more complex, that one has nothing to do with this plugin. |
| 10 | + * VRViewer is able to use this plugin though. |
| 11 | + * - A hotpoint is created and stored by user. User define its html and css. |
| 12 | + * - A hotpoint can be added to, and removed from viewer. |
| 13 | + * - Caller should set a hotpointId that is unique in the session of current viewer. |
| 14 | + * - DxfViewer doesn't maintain the relationship between hotpoint and layout. |
| 15 | + */ |
| 16 | +export class HotpointPlugin extends Plugin { |
| 17 | + protected hotpointRoot?: THREE.Group; |
| 18 | + protected css2dRenderer: CSS2DRenderer; |
| 19 | + |
| 20 | + constructor(viewer: BaseViewer) { |
| 21 | + super(viewer, { id: "HotpointPlugin" }); |
| 22 | + |
| 23 | + // eslint-disable-next-line |
| 24 | + this.css2dRenderer = (this.viewer as any).css2dRenderer; |
| 25 | + |
| 26 | + this.viewer.addEventListener(ViewerEvent.AfterRender, this.onAfterRender); |
| 27 | + } |
| 28 | + |
| 29 | + /** |
| 30 | + * @description {en} Adds a hotpoint. |
| 31 | + * Caller should set a hotpointId that is unique in the session of current viewer. |
| 32 | + * @description {zh} 添加热点。 |
| 33 | + * 调用者应该设置一个在当前Viewer会话中唯一的热点id。 |
| 34 | + * @param hotpoint |
| 35 | + * - {en} hotpoint data. |
| 36 | + * - {zh} 热点数据。 |
| 37 | + * @example |
| 38 | + * ``` typescript |
| 39 | + * const hotpoint = { |
| 40 | + * hotpointId: "c6ea70a3-ddb0-4dd0-87c8-bd2491936428", |
| 41 | + * anchorPosition: [0, 0, 0], |
| 42 | + * html: "<div>hotpoint</div>", |
| 43 | + * visible: true, |
| 44 | + * }; |
| 45 | + * const plugin = new HotpointPlugin(viewer); |
| 46 | + * plugin.add(hotpoint); |
| 47 | + * ``` |
| 48 | + */ |
| 49 | + add(hotpoint: Hotpoint): void { |
| 50 | + const exists = this.has(hotpoint.hotpointId); |
| 51 | + if (exists) { |
| 52 | + log.warn(`[Hotpoint] Hotpoint with id '${hotpoint.hotpointId}' already exist!`); |
| 53 | + return; |
| 54 | + } |
| 55 | + const p = hotpoint.anchorPosition; |
| 56 | + const object = CSS2DObjectUtils.createHotpoint(hotpoint.html); |
| 57 | + object.position.set(p[0] || 0, p[1] || 0, p[2] || 0); |
| 58 | + object.visible = hotpoint.visible !== false; |
| 59 | + object.userData.hotpoint = hotpoint; |
| 60 | + |
| 61 | + if (!this.hotpointRoot) { |
| 62 | + this.hotpointRoot = new THREE.Group(); |
| 63 | + this.hotpointRoot.matrixAutoUpdate = matrixAutoUpdate; |
| 64 | + this.hotpointRoot.matrixWorldAutoUpdate = false; |
| 65 | + this.hotpointRoot.name = "HotpointRoot"; |
| 66 | + this.viewer.scene?.add(this.hotpointRoot); |
| 67 | + } |
| 68 | + this.hotpointRoot.add(object); |
| 69 | + object.updateWorldMatrix(false, false); |
| 70 | + this.viewer.enableRender(); |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * @description {en} Removes a hotpoint by given hotpointId. |
| 75 | + * @description {zh} 根据热点id删除热点。 |
| 76 | + * @param {string} hotpointId |
| 77 | + * - {en} hotpoint id. |
| 78 | + * - {zh} 热点id。 |
| 79 | + * @example |
| 80 | + * ``` typescript |
| 81 | + * const hotpointId = "c6ea70a3-ddb0-4dd0-87c8-bd2491936428"; |
| 82 | + * const plugin = new HotpointPlugin(viewer); |
| 83 | + * plugin.remove(hotpointId); |
| 84 | + * ``` |
| 85 | + */ |
| 86 | + remove(hotpointId: string): void { |
| 87 | + const objects = this.hotpointRoot?.children || []; |
| 88 | + for (let i = 0; i < objects.length; ++i) { |
| 89 | + const obj = objects[i]; |
| 90 | + if (obj.userData.hotpoint?.hotpointId === hotpointId) { |
| 91 | + obj.removeFromParent(); |
| 92 | + } |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + /** |
| 97 | + * @description {en} Clears all hotpoints. |
| 98 | + * @description {zh} 清除所有热点。 |
| 99 | + * @example |
| 100 | + * ``` typescript |
| 101 | + * const plugin = new HotpointPlugin(viewer); |
| 102 | + * plugin.clear(); |
| 103 | + * ``` |
| 104 | + */ |
| 105 | + clear(): void { |
| 106 | + this.hotpointRoot?.clear(); |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * Checks if hotpoint with specific id already exist |
| 111 | + * Caller should set a hotpointId that is unique in the session of current DxfViewer. |
| 112 | + * @internal |
| 113 | + */ |
| 114 | + has(hotpointId: string): boolean { |
| 115 | + return !!this.findHotpointObject(hotpointId); |
| 116 | + } |
| 117 | + |
| 118 | + /** |
| 119 | + * @description {en} Moves a hotpoint. |
| 120 | + * @description {zh} 移动热点的位置。 |
| 121 | + * @example |
| 122 | + * ``` typescript |
| 123 | + * const hotpointId = "c6ea70a3-ddb0-4dd0-87c8-bd2491936428"; |
| 124 | + * const plugin = new HotpointPlugin(viewer); |
| 125 | + * plugin.move(hotpointId, [10, 10, 0]); |
| 126 | + * ``` |
| 127 | + */ |
| 128 | + move(hotpointId: string, position: Vector2 | Vector3): void { |
| 129 | + const object = this.findHotpointObject(hotpointId); |
| 130 | + if (object) { |
| 131 | + const z = (position as Vector3).z || 0; |
| 132 | + object.position.set(position.x, position.y, z); |
| 133 | + object.updateWorldMatrix(false, false); |
| 134 | + this.viewer.enableRender(); |
| 135 | + // we probably don't need to update the anchorPosition |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + /** |
| 140 | + * @description {en} Hides or show a hotpoint. |
| 141 | + * @description {zh} 显示或隐藏一个热点。 |
| 142 | + * @example |
| 143 | + * ``` typescript |
| 144 | + * const hotpointId = "c6ea70a3-ddb0-4dd0-87c8-bd2491936428"; |
| 145 | + * const plugin = new HotpointPlugin(viewer); |
| 146 | + * plugin.setVisible(hotpointId, false); |
| 147 | + * ``` |
| 148 | + */ |
| 149 | + setVisible(hotpointId: string, visible: boolean): void { |
| 150 | + const object = this.findHotpointObject(hotpointId); |
| 151 | + if (object) { |
| 152 | + object.visible = visible; |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + protected findHotpointObject(hotpointId: string): CSS2DObject | undefined { |
| 157 | + const objects = this.hotpointRoot?.children || []; |
| 158 | + const object = objects.find((obj) => obj.userData.hotpoint?.hotpointId === hotpointId); |
| 159 | + return object as CSS2DObject; |
| 160 | + } |
| 161 | + |
| 162 | + protected onAfterRender = () => { |
| 163 | + const scene = this.viewer.scene; |
| 164 | + const camera = this.viewer.camera; |
| 165 | + if (!scene || !camera || !this.hotpointRoot || this.hotpointRoot.children.length === 0) { |
| 166 | + return; |
| 167 | + } |
| 168 | + // TODO: if css2dRenderer.render() is already called in viewer, then we don't need to call it again. |
| 169 | + this.css2dRenderer?.render(scene, camera); |
| 170 | + }; |
| 171 | +} |
0 commit comments