WaterLevelLabel.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <script setup lang="ts">
  2. import { watch, computed } from 'vue'
  3. import * as THREE from 'three'
  4. import { labelTypeRegistry, type LabelDataType } from '../config/sceneConfig'
  5. import shuiliangIcon from '../assets/icon/shuiliang.png'
  6. import yingliIcon from '../assets/icon/yingli.png'
  7. const props = defineProps<{
  8. labelId: string
  9. name: string
  10. type: LabelDataType
  11. positionX: number
  12. positionY: number
  13. positionZ: number
  14. value: number
  15. }>()
  16. const iconMap: Record<string, string> = {
  17. 'shuiliang.png': shuiliangIcon,
  18. 'yingli.png': yingliIcon,
  19. }
  20. const displayConfig = computed(() => labelTypeRegistry[props.type])
  21. let scene: THREE.Scene | null = null
  22. let camera: THREE.PerspectiveCamera | null = null
  23. let textSprite: THREE.Sprite | null = null
  24. let spriteTexture: THREE.CanvasTexture | null = null
  25. let tooltipSprite: THREE.Sprite | null = null
  26. let tooltipTexture: THREE.CanvasTexture | null = null
  27. let iconReady = false
  28. let spriteCreated = false
  29. let isHovered = false
  30. const canvas = document.createElement('canvas')
  31. const ctx = canvas.getContext('2d')!
  32. const ICON_W = 100
  33. const ICON_H = 100
  34. const CANVAS_SCALE = 2
  35. canvas.width = ICON_W * CANVAS_SCALE
  36. canvas.height = ICON_H * CANVAS_SCALE
  37. const iconImg = new Image()
  38. function loadIcon() {
  39. const iconPath = displayConfig.value.icon
  40. const resolvedIcon = iconMap[iconPath]
  41. if (resolvedIcon) {
  42. iconImg.src = resolvedIcon
  43. } else {
  44. iconImg.src = shuiliangIcon
  45. }
  46. }
  47. loadIcon()
  48. iconImg.onload = () => {
  49. iconReady = true
  50. if (!spriteCreated) {
  51. ensureSprite()
  52. } else {
  53. redrawTexture()
  54. }
  55. }
  56. function redrawTexture() {
  57. ctx.setTransform(1, 0, 0, 1, 0, 0)
  58. ctx.clearRect(0, 0, canvas.width, canvas.height)
  59. ctx.drawImage(iconImg, 0, 0, canvas.width, canvas.height)
  60. ctx.setTransform(1, 0, 0, 1, 0, 0)
  61. drawText()
  62. if (spriteTexture) {
  63. spriteTexture.needsUpdate = true
  64. }
  65. }
  66. function drawText() {
  67. const cfg = displayConfig.value
  68. ctx.scale(CANVAS_SCALE, CANVAS_SCALE)
  69. ctx.textAlign = 'left'
  70. ctx.textBaseline = 'top'
  71. ctx.font = 'bold 20px "Microsoft YaHei", Arial, sans-serif'
  72. ctx.fillStyle = '#ffffff'
  73. ctx.fillText(`${props.value.toFixed(cfg.decimalPlaces)}`, 37, 16)
  74. ctx.font = '12px "Microsoft YaHei", Arial, sans-serif'
  75. ctx.fillStyle = 'rgba(255,255,255,0.8)'
  76. ctx.fillText(cfg.label, 37, 39)
  77. ctx.setTransform(1, 0, 0, 1, 0, 0)
  78. }
  79. function ensureSprite() {
  80. if (!scene || spriteCreated) return
  81. if (!iconReady) {
  82. ctx.setTransform(1, 0, 0, 1, 0, 0)
  83. ctx.clearRect(0, 0, canvas.width, canvas.height)
  84. ctx.setTransform(1, 0, 0, 1, 0, 0)
  85. drawText()
  86. } else {
  87. ctx.clearRect(0, 0, canvas.width, canvas.height)
  88. ctx.drawImage(iconImg, 0, 0, canvas.width, canvas.height)
  89. drawText()
  90. }
  91. spriteTexture = new THREE.CanvasTexture(canvas)
  92. spriteTexture.colorSpace = THREE.SRGBColorSpace
  93. spriteTexture.needsUpdate = true
  94. const material = new THREE.SpriteMaterial({
  95. map: spriteTexture,
  96. transparent: true,
  97. depthTest: false,
  98. depthWrite: false,
  99. sizeAttenuation: true,
  100. })
  101. textSprite = new THREE.Sprite(material)
  102. textSprite.position.set(props.positionX, props.positionY + 2.5 / 2, props.positionZ)
  103. textSprite.scale.set(3.0, 2.5, 1)
  104. textSprite.renderOrder = 999
  105. textSprite.name = `label_${props.labelId}`
  106. scene.add(textSprite)
  107. createTooltip()
  108. spriteCreated = true
  109. }
  110. function createTooltip() {
  111. const tooltipCanvas = document.createElement('canvas')
  112. const tooltipCtx = tooltipCanvas.getContext('2d')!
  113. const padding = 12
  114. const fontSize = 16
  115. tooltipCtx.font = `bold ${fontSize}px "Microsoft YaHei", Arial, sans-serif`
  116. const textWidth = tooltipCtx.measureText(props.name).width
  117. const tw = textWidth + padding * 2
  118. const th = fontSize + padding * 2
  119. tooltipCanvas.width = tw * CANVAS_SCALE
  120. tooltipCanvas.height = th * CANVAS_SCALE
  121. tooltipCtx.scale(CANVAS_SCALE, CANVAS_SCALE)
  122. tooltipCtx.clearRect(0, 0, tw, th)
  123. const radius = 6
  124. tooltipCtx.beginPath()
  125. tooltipCtx.moveTo(radius, 0)
  126. tooltipCtx.lineTo(tw - radius, 0)
  127. tooltipCtx.quadraticCurveTo(tw, 0, tw, radius)
  128. tooltipCtx.lineTo(tw, th - radius)
  129. tooltipCtx.quadraticCurveTo(tw, th, tw - radius, th)
  130. tooltipCtx.lineTo(radius, th)
  131. tooltipCtx.quadraticCurveTo(0, th, 0, th - radius)
  132. tooltipCtx.lineTo(0, radius)
  133. tooltipCtx.quadraticCurveTo(0, 0, radius, 0)
  134. tooltipCtx.closePath()
  135. tooltipCtx.fillStyle = 'rgba(0, 0, 0, 0.85)'
  136. tooltipCtx.fill()
  137. tooltipCtx.strokeStyle = 'rgba(255, 255, 255, 0.3)'
  138. tooltipCtx.lineWidth = 1
  139. tooltipCtx.stroke()
  140. tooltipCtx.textAlign = 'center'
  141. tooltipCtx.textBaseline = 'middle'
  142. tooltipCtx.font = `bold ${fontSize}px "Microsoft YaHei", Arial, sans-serif`
  143. tooltipCtx.fillStyle = '#ffffff'
  144. tooltipCtx.fillText(props.name, tw / 2, th / 2)
  145. tooltipTexture = new THREE.CanvasTexture(tooltipCanvas)
  146. tooltipTexture.colorSpace = THREE.SRGBColorSpace
  147. tooltipTexture.needsUpdate = true
  148. const tooltipMaterial = new THREE.SpriteMaterial({
  149. map: tooltipTexture,
  150. transparent: true,
  151. depthTest: false,
  152. depthWrite: false,
  153. sizeAttenuation: true,
  154. opacity: 0,
  155. })
  156. tooltipSprite = new THREE.Sprite(tooltipMaterial)
  157. tooltipSprite.position.set(props.positionX, props.positionY + 2.5 / 2, props.positionZ)
  158. tooltipSprite.scale.set(0, 0, 1)
  159. tooltipSprite.renderOrder = 1000
  160. tooltipSprite.name = `tooltip_${props.labelId}`
  161. if (scene) scene.add(tooltipSprite)
  162. }
  163. function init(s: THREE.Scene, c: THREE.PerspectiveCamera) {
  164. scene = s
  165. camera = c
  166. if (iconReady) {
  167. ensureSprite()
  168. } else {
  169. iconImg.onload = () => {
  170. iconReady = true
  171. ensureSprite()
  172. }
  173. }
  174. }
  175. function tick() {
  176. if (!textSprite || !camera) return
  177. const dist = camera.position.distanceTo(textSprite.position)
  178. const refDist = 50.0
  179. let s = dist / refDist
  180. const minScale = 2.0
  181. const maxScale = 20.0
  182. s = Math.max(minScale, Math.min(maxScale, s))
  183. textSprite.scale.set(s * 3.0, s * 2.2, 1)
  184. textSprite.position.y = props.positionY + (s * 2.2) / 2
  185. if (tooltipSprite) {
  186. const tooltipScaleH = s * 1.0
  187. const img = tooltipSprite.material.map!.image as HTMLImageElement
  188. const tooltipScaleW = tooltipScaleH * (img.width / img.height) * (ICON_H / ICON_W)
  189. if (isHovered) {
  190. tooltipSprite.scale.set(tooltipScaleW, tooltipScaleH, 1)
  191. tooltipSprite.position.y = props.positionY + (s * 2.2) + tooltipScaleH / 2 + 0.3
  192. } else {
  193. tooltipSprite.scale.set(0, 0, 1)
  194. }
  195. }
  196. }
  197. function dispose() {
  198. if (textSprite && scene) {
  199. scene.remove(textSprite)
  200. textSprite.material.map?.dispose()
  201. textSprite.material.dispose()
  202. textSprite = null
  203. }
  204. if (tooltipSprite && scene) {
  205. scene.remove(tooltipSprite)
  206. tooltipSprite.material.map?.dispose()
  207. tooltipSprite.material.dispose()
  208. tooltipSprite = null
  209. }
  210. if (spriteTexture) {
  211. spriteTexture = null
  212. }
  213. if (tooltipTexture) {
  214. tooltipTexture = null
  215. }
  216. spriteCreated = false
  217. iconReady = false
  218. isHovered = false
  219. }
  220. watch(() => props.value, () => {
  221. if (spriteCreated) {
  222. redrawTexture()
  223. }
  224. })
  225. function setHovered(hovered: boolean) {
  226. isHovered = hovered
  227. if (tooltipSprite) {
  228. const mat = tooltipSprite.material as THREE.SpriteMaterial
  229. mat.opacity = hovered ? 1 : 0
  230. }
  231. }
  232. function getSprite(): THREE.Sprite | null {
  233. return textSprite
  234. }
  235. function getPosition(): { x: number; y: number; z: number } {
  236. return { x: props.positionX, y: props.positionY, z: props.positionZ }
  237. }
  238. defineExpose({ init, tick, dispose, setHovered, getSprite, getPosition })
  239. </script>
  240. <template>
  241. </template>