three-base 2.0.137 → 2.0.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/DirectionalLight.d.ts +2 -2
  2. package/DirectionalLight.js +1 -1
  3. package/DirectionalLight.ts +70 -0
  4. package/EventMap.d.ts +103 -0
  5. package/EventMap.js +17 -0
  6. package/EventMap.ts +139 -0
  7. package/EventMesh.ts +60 -0
  8. package/ExtrudeMesh.js +1 -25
  9. package/ExtrudeMesh.ts +553 -0
  10. package/FPScontrol.d.ts +448 -15
  11. package/FPScontrol.js +1448 -266
  12. package/FPScontrol.ts +1855 -0
  13. package/Fire/Fire.d.ts +4 -4
  14. package/Fire/Fire.js +8 -7
  15. package/Fire/Fire.ts +89 -0
  16. package/Fire/{FireGromtry.d.ts → FireGeometry.d.ts} +2 -2
  17. package/Fire/{FireGromtry.js → FireGeometry.js} +1 -1
  18. package/Fire/FireGeometry.ts +40 -0
  19. package/Fire/FireMaterial.d.ts +3 -3
  20. package/Fire/FireMaterial.js +1 -1
  21. package/Fire/FireMaterial.ts +89 -0
  22. package/Fire/index.d.ts +1 -0
  23. package/Fire/index.js +1 -0
  24. package/Fire/index.ts +2 -0
  25. package/GLTFDracoLoader.d.ts +71 -0
  26. package/GLTFDracoLoader.js +266 -0
  27. package/GLTFDracoLoader.ts +430 -0
  28. package/Grass/Grass.d.ts +2 -2
  29. package/Grass/Grass.ts +38 -0
  30. package/Grass/index.d.ts +1 -0
  31. package/Grass/index.js +1 -0
  32. package/Grass/index.ts +2 -0
  33. package/Ground1/Ground1.d.ts +2 -3
  34. package/Ground1/Ground1.js +64 -60
  35. package/Ground1/Ground1.ts +114 -0
  36. package/Ground1/index.ts +1 -0
  37. package/MeshBase.d.ts +1 -0
  38. package/MeshBase.js +37 -13
  39. package/MeshBase.ts +253 -0
  40. package/Mirror.d.ts +3 -3
  41. package/Mirror.js +7 -3
  42. package/Mirror.ts +203 -0
  43. package/MktMesh/MktMesh.d.ts +301 -0
  44. package/MktMesh/MktMesh.js +1064 -0
  45. package/MktMesh/MktMesh.ts +1096 -0
  46. package/MktMesh/assetUrl.d.ts +59 -0
  47. package/MktMesh/assetUrl.js +154 -0
  48. package/MktMesh/assetUrl.ts +155 -0
  49. package/MktMesh/mktMappingShader.d.ts +12 -0
  50. package/MktMesh/mktMappingShader.js +139 -0
  51. package/MktMesh/mktMappingShader.ts +139 -0
  52. package/MktMesh/mktShader.d.ts +263 -0
  53. package/MktMesh/mktShader.js +1248 -0
  54. package/MktMesh/mktShader.ts +1344 -0
  55. package/MktMesh/panoBitmapTexture.d.ts +25 -0
  56. package/MktMesh/panoBitmapTexture.js +52 -0
  57. package/MktMesh/panoBitmapTexture.ts +54 -0
  58. package/MktMesh/panoSlotBind.d.ts +29 -0
  59. package/MktMesh/panoSlotBind.js +106 -0
  60. package/MktMesh/panoSlotBind.ts +153 -0
  61. package/MktMesh/panoSlotRoster.d.ts +22 -0
  62. package/MktMesh/panoSlotRoster.js +169 -0
  63. package/MktMesh/panoSlotRoster.ts +168 -0
  64. package/MktMesh/panoTileStitcher.d.ts +119 -0
  65. package/MktMesh/panoTileStitcher.js +533 -0
  66. package/MktMesh/panoTileStitcher.ts +616 -0
  67. package/MktMesh/types.d.ts +75 -0
  68. package/MktMesh/types.js +1 -0
  69. package/MktMesh/types.ts +87 -0
  70. package/MktMesh/wallCapTessellate.d.ts +33 -0
  71. package/MktMesh/wallCapTessellate.js +97 -0
  72. package/MktMesh/wallCapTessellate.ts +68 -0
  73. package/PipeMesh/PathGeometry.js +9 -2
  74. package/PipeMesh/PathGeometry.ts +343 -0
  75. package/PipeMesh/PathPoint.ts +36 -0
  76. package/PipeMesh/PathPointList.js +1 -0
  77. package/PipeMesh/PathPointList.ts +227 -0
  78. package/PipeMesh/PathTubeGeometry.ts +146 -0
  79. package/PipeMesh/PipeMesh.d.ts +1 -1
  80. package/PipeMesh/PipeMesh.js +1 -1
  81. package/PipeMesh/PipeMesh.ts +76 -0
  82. package/PipeMesh/index.ts +1 -0
  83. package/PlaneModal1/PlaneModal1.d.ts +16 -13
  84. package/PlaneModal1/PlaneModal1.js +4 -0
  85. package/PlaneModal1/PlaneModal1.ts +138 -0
  86. package/PlaneModal1/index.ts +1 -0
  87. package/PlaneText.d.ts +2 -14
  88. package/PlaneText.js +7 -2
  89. package/PlaneText.ts +55 -0
  90. package/PlaneWithHtml.d.ts +4 -2
  91. package/PlaneWithHtml.js +2 -1
  92. package/PlaneWithHtml.ts +70 -0
  93. package/PointLight/PointLight.d.ts +3 -4
  94. package/PointLight/PointLight.js +1 -7
  95. package/PointLight/PointLight.ts +67 -0
  96. package/PointLight/index.ts +1 -0
  97. package/README.md +110 -79
  98. package/Sky/Sky.ts +22 -0
  99. package/Sky/index.ts +1 -0
  100. package/SpotLight.d.ts +2 -2
  101. package/SpotLight.js +1 -1
  102. package/SpotLight.ts +74 -0
  103. package/SpriteModal1/SpriteModal1.d.ts +16 -13
  104. package/SpriteModal1/SpriteModal1.js +8 -2
  105. package/SpriteModal1/SpriteModal1.ts +146 -0
  106. package/SpriteModal1/index.ts +1 -0
  107. package/SpriteText.d.ts +14 -13
  108. package/SpriteText.js +22 -9
  109. package/SpriteText.ts +205 -0
  110. package/SpriteText2.d.ts +9 -8
  111. package/SpriteText2.js +57 -32
  112. package/SpriteText2.ts +258 -0
  113. package/SpriteText3.d.ts +86 -0
  114. package/SpriteText3.js +519 -0
  115. package/SpriteText3.ts +599 -0
  116. package/SpriteWithHtml.d.ts +4 -2
  117. package/SpriteWithHtml.ts +50 -0
  118. package/Text3D/Text3D.js +17 -21
  119. package/Text3D/Text3D.ts +77 -0
  120. package/Text3D/fontController.d.ts +10 -9
  121. package/Text3D/fontController.js +17 -50
  122. package/Text3D/fontController.ts +209 -0
  123. package/Text3D/index.ts +1 -0
  124. package/ThreeBase.d.ts +168 -31
  125. package/ThreeBase.js +663 -117
  126. package/ThreeBase.ts +1314 -0
  127. package/ThreeMouse.d.ts +20 -7
  128. package/ThreeMouse.js +78 -32
  129. package/ThreeMouse.ts +472 -0
  130. package/Transition.d.ts +12 -7
  131. package/Transition.js +26 -27
  132. package/Transition.ts +242 -0
  133. package/Water/Water.js +21 -3
  134. package/Water/Water.ts +45 -0
  135. package/Water/index.d.ts +2 -0
  136. package/Water/index.js +2 -0
  137. package/Water/index.ts +2 -0
  138. package/WaterFlow/FloatingDuck.d.ts +88 -0
  139. package/WaterFlow/FloatingDuck.js +307 -0
  140. package/WaterFlow/FloatingDuck.ts +445 -0
  141. package/WaterFlow/GPUFluidSimulation.d.ts +56 -0
  142. package/WaterFlow/GPUFluidSimulation.js +785 -0
  143. package/WaterFlow/GPUFluidSimulation.ts +827 -0
  144. package/WaterFlow/ReservoirSystem.d.ts +106 -0
  145. package/WaterFlow/ReservoirSystem.js +621 -0
  146. package/WaterFlow/ReservoirSystem.ts +754 -0
  147. package/WaterFlow/SPHField.d.ts +35 -0
  148. package/WaterFlow/SPHField.js +204 -0
  149. package/WaterFlow/SPHField.ts +228 -0
  150. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -0
  151. package/WaterFlow/ScreenSpaceFluidPass.js +736 -0
  152. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -0
  153. package/WaterFlow/ShallowWaterSurface.d.ts +45 -0
  154. package/WaterFlow/ShallowWaterSurface.js +441 -0
  155. package/WaterFlow/ShallowWaterSurface.ts +510 -0
  156. package/WaterFlow/WaterFlow.d.ts +87 -0
  157. package/WaterFlow/WaterFlow.js +171 -0
  158. package/WaterFlow/WaterFlow.ts +219 -0
  159. package/WaterFlow/types.d.ts +161 -0
  160. package/WaterFlow/types.js +1 -0
  161. package/WaterFlow/types.ts +189 -0
  162. package/WaterFlow/waterFlowPass.d.ts +3 -0
  163. package/WaterFlow/waterFlowPass.js +20 -0
  164. package/WaterFlow/waterFlowPass.ts +40 -0
  165. package/collisionOBB.d.ts +42 -0
  166. package/collisionOBB.js +112 -0
  167. package/collisionOBB.ts +108 -0
  168. package/declare.d.ts +25 -167
  169. package/index.js +0 -1
  170. package/index.ts +1 -0
  171. package/instanced/InstancedBatch.d.ts +57 -0
  172. package/instanced/InstancedBatch.js +348 -0
  173. package/instanced/InstancedBatch.ts +406 -0
  174. package/instanced/InstancedChild.d.ts +174 -0
  175. package/instanced/InstancedChild.js +264 -0
  176. package/instanced/InstancedChild.ts +327 -0
  177. package/instanced/InstancedGroup.d.ts +43 -0
  178. package/instanced/InstancedGroup.js +126 -0
  179. package/instanced/InstancedGroup.ts +137 -0
  180. package/{InstancedMeshBase.d.ts → instanced/InstancedMeshBase.d.ts} +6 -7
  181. package/{InstancedMeshBase.js → instanced/InstancedMeshBase.js} +36 -47
  182. package/instanced/InstancedMeshBase.ts +292 -0
  183. package/instanced/InstancedTemplate.d.ts +46 -0
  184. package/instanced/InstancedTemplate.js +125 -0
  185. package/instanced/InstancedTemplate.ts +188 -0
  186. package/keyControls.d.ts +29 -0
  187. package/{keyContorls.js → keyControls.js} +10 -7
  188. package/keyControls.ts +107 -0
  189. package/lensflare/LensFlare.d.ts +39 -0
  190. package/lensflare/LensFlare.js +373 -0
  191. package/lensflare/LensFlare.ts +448 -0
  192. package/lensflare/lensflare0.png +0 -0
  193. package/lensflare/lensflare3.png +0 -0
  194. package/lensflare/lensflare_custom.png +0 -0
  195. package/lensflare/lensflare_hex_ghost.png +0 -0
  196. package/lensflare/lensflare_hex_main.png +0 -0
  197. package/package.json +104 -25
  198. package/pass/bloom_pass.d.ts +1 -1
  199. package/pass/bloom_pass.js +127 -40
  200. package/pass/bloom_pass.ts +243 -0
  201. package/pass/cover_pass.d.ts +1 -1
  202. package/pass/cover_pass.js +55 -32
  203. package/pass/cover_pass.ts +182 -0
  204. package/pass/debugDepthState.d.ts +20 -0
  205. package/pass/debugDepthState.js +9 -0
  206. package/pass/debugDepthState.ts +22 -0
  207. package/pass/depthTestBias.d.ts +12 -0
  208. package/pass/depthTestBias.js +12 -0
  209. package/pass/depthTestBias.ts +12 -0
  210. package/pass/effectUserData.d.ts +27 -0
  211. package/pass/effectUserData.js +1 -0
  212. package/pass/effectUserData.ts +46 -0
  213. package/pass/frosted_glass_pass.d.ts +1 -1
  214. package/pass/frosted_glass_pass.js +45 -64
  215. package/pass/frosted_glass_pass.ts +288 -0
  216. package/pass/fsQuad.d.ts +2 -2
  217. package/pass/fsQuad.ts +34 -0
  218. package/pass/glow_pass.d.ts +1 -1
  219. package/pass/glow_pass.js +288 -72
  220. package/pass/glow_pass.ts +654 -0
  221. package/pass/screen_bloom_pass.d.ts +11 -0
  222. package/pass/screen_bloom_pass.js +235 -0
  223. package/pass/screen_bloom_pass.ts +263 -0
  224. package/pass/ssao_pass.d.ts +78 -3
  225. package/pass/ssao_pass.js +642 -206
  226. package/pass/ssao_pass.ts +918 -0
  227. package/pass/stroke_pass.d.ts +1 -1
  228. package/pass/stroke_pass.js +187 -116
  229. package/pass/stroke_pass.ts +638 -0
  230. package/shaders/FXAAShaderMaterial.js +5 -48
  231. package/shaders/FXAAShaderMaterial.ts +201 -0
  232. package/shaders/baseDepthVisShader.d.ts +11 -0
  233. package/shaders/baseDepthVisShader.js +36 -0
  234. package/shaders/baseDepthVisShader.ts +36 -0
  235. package/shaders/baseTextureToRenderShader.js +46 -51
  236. package/shaders/baseTextureToRenderShader.ts +232 -0
  237. package/shaders/copyShader.js +2 -3
  238. package/shaders/copyShader.ts +32 -0
  239. package/shaders/debugDepthCompareShader.d.ts +14 -0
  240. package/shaders/debugDepthCompareShader.js +49 -0
  241. package/shaders/debugDepthCompareShader.ts +50 -0
  242. package/shaders/debugDepthJudgeShader.d.ts +18 -0
  243. package/shaders/debugDepthJudgeShader.js +52 -0
  244. package/shaders/debugDepthJudgeShader.ts +54 -0
  245. package/shaders/debugExcludedShader.d.ts +13 -0
  246. package/shaders/debugExcludedShader.js +46 -0
  247. package/shaders/debugExcludedShader.ts +48 -0
  248. package/spriteTextBg.d.ts +1 -0
  249. package/spriteTextBg.js +20 -0
  250. package/spriteTextBg.ts +20 -0
  251. package/threeLoader.d.ts +8 -7
  252. package/threeLoader.js +58 -67
  253. package/threeLoader.ts +525 -0
  254. package/threeUtils.d.ts +2 -1
  255. package/threeUtils.js +24 -25
  256. package/threeUtils.ts +373 -0
  257. package/types.d.ts +2 -0
  258. package/types.js +6 -0
  259. package/types.ts +8 -0
  260. package/ApngTexture.d.ts +0 -13
  261. package/ApngTexture.js +0 -61
  262. package/ApngTextureManage.d.ts +0 -18
  263. package/ApngTextureManage.js +0 -87
  264. package/KeyFrameAnimation.d.ts +0 -25
  265. package/KeyFrameAnimation.js +0 -78
  266. package/keyContorls.d.ts +0 -25
  267. package/lib/draco/draco_decoder.js +0 -34
  268. package/lib/draco/draco_decoder.wasm +0 -0
  269. package/lib/draco/draco_wasm_wrapper.js +0 -117
  270. package/pass/emissive_ao_pass.d.ts +0 -13
  271. package/pass/emissive_ao_pass.js +0 -370
  272. package/shaders.d.ts +0 -5
  273. package/shaders.js +0 -93
package/SpriteText3.ts ADDED
@@ -0,0 +1,599 @@
1
+ /*
2
+ * @Author: xiaosihan
3
+ * @Date: 2026-09-02(从旧 9-slice + Canvas 画箭头 重构为 11-slice 源图切片)
4
+ * @Description: 带源图气泡背景的精灵文字组件。
5
+ *
6
+ * ============================================================================
7
+ * 【11-slice 气泡背景切片方案】(2026-09-02 用户硬性要求,硬性写进注释防遗忘)
8
+ * ============================================================================
9
+ *
10
+ * 原始需求(防止改着改着就忘记原始需求):
11
+ * 把气泡背景源图 spriteTextBg.png 切分成 11 个部分,绘制到 canvas:
12
+ * [1] 左上角 — 不可变形(保持原始宽高比,等比缩放)
13
+ * [2] 上边 — 横向拉伸
14
+ * [3] 右上角 — 不可变形
15
+ * [4] 左边 — 纵向拉伸
16
+ * [5] 中间 — 双向拉伸
17
+ * [6] 右边 — 纵向拉伸
18
+ * [7] 左下角 — 不可变形
19
+ * [8] 左中连接边 — 横向拉伸
20
+ * [9] 下中尖角 — 不可变形(含气泡底边 + 箭头)
21
+ * [10] 中右连接边 — 横向拉伸
22
+ * [11] 右下角 — 不可变形
23
+ *
24
+ * 不可变形块 = 5 个:左上、右上、左下、下中尖角、右下。
25
+ * 拉伸块 = 6 个:上边、左边、中间、右边、左中连接边、中右连接边。
26
+ *
27
+ * 源图坐标(2026-09-06 皇上指定:原始设计稿 922×400;硬编码常量,如换源图必须同步改):
28
+ * - 切片范围 = 整图(含外辉光):alpha>0 区域 x[0,922) y[0,392)
29
+ * - 不透明本体(深蓝圆角矩形 + 蓝霓虹描边,alpha==255):x[42,880] y[44,344](描边底 y=345)
30
+ * - 圆角半径 38;边框带 SRC_CORNER = 辉光 42 + 圆角 38 = 82(角块必须含完整辉光外角 + 圆角)
31
+ * - 底部箭头:青色倒三角,顶边 y=346 宽 49(x 447..495),尖点 (471, 386),高 41
32
+ * - 外辉光:蓝色 (64,86,255)、alpha 1~62,分布在本体四周 → 由 11-slice 边框带承载(拉伸)
33
+ * - ⚠️ 演变史:最初误用 [5,918]×[7,382](alpha>0 外沿)切片→辉光错位混入→发灰+透明度二次合成;
34
+ * 随后改 alpha==255 本体切片修掉发灰,但把辉光全裁掉了(皇上:「图片没有用完,
35
+ * 裁掉了很大一部分边缘的图片纹理」);最终方案 = 整图切片 + 边框带 82(9-patch 标准)。
36
+ *
37
+ * 11-slice 网格(以源图坐标定义):
38
+ *
39
+ * 列(x 方向,5 列):
40
+ * C0 左角列 [SRC_BODY_LEFT, SRC_BODY_LEFT + SRC_CORNER) 宽 c
41
+ * C1 左连接列 [SRC_BODY_LEFT + SRC_CORNER, 画布水平中心对应的源列) (横向拉伸源段)
42
+ * C2 尖角列 [TIP_SRC_X, TIP_SRC_X + TIP_SRC_W) 宽 TIP_SRC_W=60 (固定列,以箭头为中心)
43
+ * C3 右连接列 [画布水平中心对应的源列, SRC_BODY_RIGHT - SRC_CORNER) (横向拉伸源段)
44
+ * C4 右角列 [SRC_BODY_RIGHT - SRC_CORNER, SRC_BODY_RIGHT) 宽 c
45
+ *
46
+ * 行(y 方向,4 行):
47
+ * R0 上角行 [SRC_BODY_TOP, SRC_BODY_TOP + SRC_CORNER) 高 c
48
+ * R1 中间行 [SRC_BODY_TOP + SRC_CORNER, SRC_BODY_BOTTOM - SRC_CORNER) 高 (双向拉伸)
49
+ * R2 下角行 [SRC_BODY_BOTTOM - SRC_CORNER, SRC_BODY_BOTTOM) 高 c ← 含气泡底边
50
+ * R3 箭头行 [SOLID_BOTTOM, SOLID_BOTTOM + ARROW_SRC_H) 高 ARROW_SRC_H = 41 ← 源图箭头凸出(被辉光尾包住)
51
+ *
52
+ * 部件 → 网格映射(绘制顺序即下述顺序,尖角块最后叠加):
53
+ * [1] 左上角 = (R0, C0) 不可变形,cornerSize × cornerSize
54
+ * [2] 上边 = (R0, C1+C2+C3) 横向拉伸(高 cornerSize)
55
+ * [3] 右上角 = (R0, C4) 不可变形
56
+ * [4] 左边 = (R1, C0) 纵向拉伸(宽 cornerSize)
57
+ * [5] 中间 = (R1, C1+C2+C3) 双向拉伸
58
+ * [6] 右边 = (R1, C4) 纵向拉伸
59
+ * [7] 左下角 = (R2, C0) 不可变形
60
+ * [8] 左中连接边 = (R2, C1+C2+C3 左半) 横向拉伸,从左下角右缘画到 canvas 水平中心
61
+ * [10] 中右连接边 = (R2, C1+C2+C3 右半) 横向拉伸,从 canvas 水平中心画到右下角左缘
62
+ * [11] 右下角 = (R2, C4) 不可变形
63
+ * [9] 下中尖角 = (R2 下沿粘合段 + R3, C2) 不可变形,等比缩放,最后叠加在底边带中央之上
64
+ *
65
+ * 关键设计点:
66
+ * 1) 尖角块源区域 = C2 列,且【纵向只取箭头本身】,不取任何底边描边行:
67
+ * y ∈ [SOLID_BOTTOM + 1, SOLID_BOTTOM + 1 + TIP_SRC_H) = [346, 389)
68
+ * 高度 = TIP_SRC_H = ARROW_SRC_H(41) + ARROW_TIP_FADE(2) = 43 源像素。
69
+ * ⚠️ 起点必须是描边底 SOLID_BOTTOM(345) 的【下一行】346:cell 若向上多含 345 一行,
70
+ * 就会用箭头的亮边把下边框描边的【最后一行】覆盖 → 皇上反馈的
71
+ * 「箭头和下边框有 1px 的重叠」(2026-09-14 真实 Chrome 逐行实测确认)。
72
+ * ⚠️ 不能把 R2 行整个包进尖角块:尖角块整体等比缩放(不可变形),
73
+ * 若向上多包描边行,放大后尖角块会跑出 canvas 被裁剪 → 底边带中间留洞(2026-09-02 踩坑)。
74
+ * 2) 尖角块在画布上等比缩放(不变形),缩放比由 arrowTargetH 决定:
75
+ * tipScale = arrowTargetH / ARROW_SRC_H(默认 41/41 = 1:1 最锐利)
76
+ * 尖角块画布尺寸 = (TIP_SRC_W * tipScale) × (TIP_SRC_H * tipScale)。
77
+ * arrowTargetH ≤ 41(ARROW_SRC_H)时均为缩小/1:1,不会糊;调大超过 41 才开始放大。
78
+ * 3) 尖角块定位:
79
+ * solidBottomDst = bodyHc - cornerSize*(GLOW_TAIL/SRC_CORNER) // 描边底在画布上的 y
80
+ * dstX = bodyWc/2 - tipDstW/2 + arrowOffsetX // 以箭头为中心水平居中 + 视觉微调
81
+ * dstY = solidBottomDst + 1 // 箭头紧贴描边底的下一行(不与描边重叠)
82
+ * canvasH = max(bodyHc, 箭头尖 y):默认箭头尖埋在底部辉光带内(与原图一致),
83
+ * arrowTargetH 调大超出辉光带时画布自动加高。
84
+ * 4) 左中连接边[8] / 中右连接边[10] 【横跨整个底边带】(到 canvas 水平中心为止),
85
+ * 尖角块[9]【最后 clearRect + source-over 绘制】在底边带中央之上:cell 与连接边同源、同 1:1 比例,
86
+ * 故两侧视觉连续。⚠️ 用 clearRect("替换")而非叠加 → 不存在透明度二次合成,也不需要粘合段;
87
+ * 箭头两侧源像素即底边辉光,与连接边同色 → 底边带完整无缝,中间不会留洞。
88
+ * 5) canvas 总高 = max(bodyHc, 箭头尖 y)(bodyHc 之上是气泡主体,之下是箭头 + 辉光尾)。
89
+ * 6) 文字居中于 bodyHc/2(气泡主体几何中心,不在 canvas 中心,因为下方有箭头凸出)。
90
+ * 7) 源图箭头中心 x=471 相对气泡中心 461 偏右约 10px;尖角块以【箭头】为中心取块,
91
+ * 画布上块居中 → 箭头相对气泡居中(偏置被自然消除),如需偏移用 arrowOffsetX 微调。
92
+ * 8) 颜色空间(2026-09-06 修):canvas 纹理必须 texture.colorSpace = SRGBColorSpace(采样时
93
+ * 硬件解码 sRGB→linear),片元着色器输出前 #include <colorspace_fragment>(按渲染目标
94
+ * 自动编码)。缺任一环 → 主场景后处理链二次编码 → 整体发白偏亮。
95
+ *
96
+ * 旧实现对照(已废弃,2026-09-02 之前):
97
+ * - 用 Canvas2D 单独绘制等腰三角作为箭头(避开源图箭头非对称问题)
98
+ * - 新方案按用户要求改为"源图箭头作为尖角块不可变形",源图非对称特征自然保留为视觉特色
99
+ * - 旧 bubbleAlpha / bgColor / borderFrac / borderTopScale+BottomScale+LeftScale+RightScale
100
+ * 4 个 scale 属性一并移除(源图自带颜色不可调,圆角半径单一控制更简洁)
101
+ *
102
+ * 边界处理 / 已知细节:
103
+ * - 源图箭头尖在 (471, 386);尖角块以箭头为中心取块,画布上自动相对气泡居中
104
+ * - 拉伸源段([2][4][5][6][8][10])取【从一角块边界到另一角块边界的完整连续区段】
105
+ * (9-patch 标准做法):使相邻块在拼接处的源像素相邻/镜像对称 → 无接缝。
106
+ * ⚠️ 2026-09-14 修正(皇上:「左上角圆角很正常,但其他 3 个圆角的拼接不对;三角形区域
107
+ * 左右 2 边的像素点都没拼接上,可以看到明显的接缝」):
108
+ * 原实现误把拉伸源段当成「左上角内侧固定 82px 的小段」来拉伸,导致除左上角外的每条
109
+ * 拼接边两侧源像素相隔上百像素——中间行底端源 y163 ↔ 底边带顶源 y310(跨 147px),
110
+ * 上边带右端源 x163 ↔ 右上角块源 x840;而源图本体底部 y≈300 起有明显内发光
111
+ * ((9,27,58) → (12,32,74) → y330 (23,51,114)),于是画布 y=bodyHc-cornerSize 处出现横向
112
+ * 台阶(左下/右下角接缝)、x=bodyWc-cornerSize 处出现纵向台阶(右上角接缝)、底边带中心
113
+ * 源 x163↔x758 与尖角块源 x441/501 不连续(箭头两侧接缝)。
114
+ * 改为整段取源后,所有相邻边界源像素连续(y309↔y310、x839↔x840、x440↔x441、x500↔x501)。
115
+ * - 辉光带在边框带内被拉伸(辉光是垂直于边缘的渐变、沿边缘方向均匀 → 拉伸不失真)
116
+ * - 文字居中于 bodyHc/2 ≈ 不透明本体中心(辉光带上下近似对称,偏差 <2% 可忽略)
117
+ * - 尖角块粘合段取自不透明底边描边 → 与连接边叠加处双方 alpha=255,无透明度二次合成
118
+ * - canvas resize 时浏览器会清空画布,每次 _genText 都从 clearRect 0,0,canvasW,canvasH 开始
119
+ *
120
+ * 后续修改警告(再改此文件前必读本注释):
121
+ * - 改源图 spriteTextBg.png 尺寸/箭头位置/圆角半径 → 同步更新 SRC_* 常量
122
+ * - 改 11 个部件的命名或数量 → 先确认用户是否同意(用户明确说"防止改着改着就忘记原始需求")
123
+ * - 尖角块若改成"可拉伸"(如 9-slice 底边中段一样)→ 需重新讨论,原始需求是"不可变形"
124
+ * ============================================================================
125
+ */
126
+ import {
127
+ DoubleSide,
128
+ LinearFilter,
129
+ SRGBColorSpace,
130
+ Mesh,
131
+ PlaneGeometry,
132
+ ShaderMaterial,
133
+ Texture,
134
+ } from "three";
135
+ import { defaultBg } from "./spriteTextBg";
136
+ import type { ColorString } from "./types";
137
+
138
+ /** 顶点着色器:Mesh billboard,plane 永远正对相机,尺寸由 mesh.scale 驱动 */
139
+ const VERTEX_SHADER = /* glsl */ `
140
+ #include <common>
141
+ #include <logdepthbuf_pars_vertex>
142
+ varying vec2 vUv;
143
+ void main() {
144
+ vUv = uv;
145
+ vec2 scale = vec2(length(modelMatrix[0].xyz), length(modelMatrix[1].xyz));
146
+ vec4 mvPosition = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
147
+ mvPosition.xy += position.xy * scale;
148
+ gl_Position = projectionMatrix * mvPosition;
149
+ #include <logdepthbuf_vertex>
150
+ }
151
+ `;
152
+
153
+ /** 片元着色器:直接输出 canvas 纹理(气泡 + 文字已合好) */
154
+ const FRAGMENT_SHADER = /* glsl */ `
155
+ #include <common>
156
+ #include <logdepthbuf_pars_fragment>
157
+ uniform sampler2D uTextTexture;
158
+ varying vec2 vUv;
159
+ void main() {
160
+ gl_FragColor = texture2D(uTextTexture, vUv);
161
+ // 纹理标记 SRGBColorSpace(采样时硬件解码 sRGB→linear),
162
+ // 此处按渲染目标自动编码输出:主场景线性 RT 下 passthrough,直接出屏时编码 sRGB。
163
+ // ⚠️ 不加会导致主场景后处理链终处理二次编码 → 整体发白偏亮(2026-09-06 修)。
164
+ #include <colorspace_fragment>
165
+ #include <logdepthbuf_fragment>
166
+ }
167
+ `;
168
+
169
+ /** 1x1 透明占位纹理,纹理生成完成前使用 */
170
+ function createPlaceholderTexture(): Texture {
171
+ const canvas = document.createElement("canvas");
172
+ canvas.width = 1;
173
+ canvas.height = 1;
174
+ const texture = new Texture(canvas);
175
+ texture.colorSpace = SRGBColorSpace;
176
+ texture.needsUpdate = true;
177
+ return texture;
178
+ }
179
+
180
+ /**
181
+ * 源图硬编码常量(详见文件头注释)。
182
+ *
183
+ * ⚠️ 2026-09-06 皇上旨:源图 = 原始设计稿 spriteTextBg.png(922×400),"不要动图片了"
184
+ * —— 图片禁止任何加工(不缩放/不重绘/不调色),只调切片常量。
185
+ * ⚠️ 2026-09-06 二次反馈「图片没有用完,裁掉了很大一部分边缘」:
186
+ * 曾按 alpha==255 本体(x[42,880] y[44,345])切片,把外圈辉光全裁掉了。
187
+ * 现改为 9-patch 标准做法:**整图(含辉光)纳入切片**,边框带 = 辉光 + 圆角 + 描边。
188
+ * 本常量为原图实测值(PIL 逐像素量测):
189
+ * alpha>0 区域 x[0,922) y[0,392);圆角 38(圆心 80,82);描边底(不透明本体底)y=345;
190
+ * 辉光尾 345..392;箭头顶边 y=346 宽 49(x 447..495)、尖点 (471,386)。
191
+ * 如换源图必须同步改本常量。
192
+ */
193
+ const SRC_W = 922;
194
+ const SRC_H = 400;
195
+ /** 切片范围 = 整图(含外辉光):alpha>0 区域 x[0,922) y[0,392)(exclusive 右/下沿) */
196
+ const SRC_BODY_LEFT = 0;
197
+ const SRC_BODY_RIGHT = 922;
198
+ const SRC_BODY_TOP = 0;
199
+ const SRC_BODY_BOTTOM = 392;
200
+ /**
201
+ * 边框带厚度(4 角方块的源边长)= 外辉光 42 + 圆角 38 = 82。
202
+ * 角块必须包含完整辉光外角 + 完整圆角,二者缺一会被拉伸变形(9-patch 标准)。
203
+ */
204
+ const SRC_CORNER = 82;
205
+ /** 不透明本体(描边)底 y=345;描边底到辉光底的"辉光尾"高 47(底边带内源像素) */
206
+ const SOLID_BOTTOM = 345;
207
+ const GLOW_TAIL = SRC_BODY_BOTTOM - SOLID_BOTTOM;
208
+ /** 底部箭头源像素尺寸(顶边宽 49 的青色倒三角,顶边 y=346 贴气泡底边、尖点 (471,386)) */
209
+ const ARROW_SRC_W = 49;
210
+ const ARROW_SRC_H = 41;
211
+ /** 箭头源中心 x(源图特征:相对气泡中心 461 偏右约 10px;尖角块以箭头为中心取块,画布上自动居中) */
212
+ const ARROW_SRC_CX = 471;
213
+ /** 尖角块源宽度(以箭头为中心,向两侧各取 30 像素 = 60 像素宽,覆盖箭头顶边 49 + 余量) */
214
+ const TIP_SRC_W = 60;
215
+ /** 尖角块源 x 起点 = 471 - 30 = 441 */
216
+ const TIP_SRC_X = ARROW_SRC_CX - TIP_SRC_W / 2;
217
+ /**
218
+ * 尖角块源 y 起点 = 描边底 SOLID_BOTTOM(345) 的【下一行】346(箭头顶边)。
219
+ * ⚠️ 必须从 346 起取,不可向上多含 345 一行:345 是下边框描边的【最后一行】,
220
+ * cell 若含它,就用箭头的亮边把该行覆盖 → 皇上反馈的「箭头和下边框有 1px 的重叠」
221
+ * (2026-09-14 真实 Chrome 逐行实测:该行在无箭头时全宽 (47,118,215),
222
+ * 含箭头后被覆盖为 (72,186,248))。
223
+ * 说明:本块用 clearRect + source-over 绘制("替换"而非"叠加"),不会发生透明度二次合成,
224
+ * 故不需要"粘合段";旧的 TIP_SRC_OVERLAP=4 及其粘合段已删除(2026-09-14)。
225
+ */
226
+ const TIP_SRC_Y = SOLID_BOTTOM + 1;
227
+ /**
228
+ * 箭头源区额外多取的"尖点渐隐"行数:源 y387(α188) / y388(α21) 仍是箭头色
229
+ * (y389 起才是底色辉光 (64,86,255,α≤3)),属尖点抗锯齿。少取则尖点末尾出现 1px 硬切。
230
+ */
231
+ const ARROW_TIP_FADE = 2;
232
+ /** 尖角块源高度 = 41(箭头主体 346..386) + 2(尖点渐隐 387..388) = 43 */
233
+ const TIP_SRC_H = ARROW_SRC_H + ARROW_TIP_FADE;
234
+
235
+ /**
236
+ * 箭头(2026-09-14 皇上旨"箭头看不清楚了",前一轮"接缝"已由 11-slice 连续取源修好):
237
+ * 源图箭头 cell(TIP_SRC_X=441…)左右边界 x441/500 与左右带子 [8]/[10] 逐行 RGBA 完全一致(均带子蓝),
238
+ * 且 cell 是整块青蓝渐变:边缘=带子蓝低透明(α≈32~43)、箭身/尖点=亮青(≈47,168,225)高透明(α≈248)。
239
+ * [9] 用 **「先 clearRect 局部清空 + source-over 画原始 cell」**(等价"局部 copy"):
240
+ * · 清空:先清掉 [9] 矩形内的带子 → 箭头边距不会与带子 α 相加 → 无缝;
241
+ * · 绘制:source-over 画原始 cell,箭身=亮青 α≈248 → 清晰可见。
242
+ * ⚠️ 三个已验证的坑(2026-09-14 各踩一次,真实 Chrome 无头实测):
243
+ * · source-over:边距 α 与带子 α 相加(32+33≈60) → 又现台阶(皇上反馈的"接缝");
244
+ * · source-atop:箭头 α 被压到带子的≈43 → 整块与带子同透明 → 箭头糊没(皇上反馈"看不清楚");
245
+ * · **copy:canvas 的 copy 是"清空整个画布、只保留本次绘制"**(不是只替换目标矩形!)→
246
+ * 前面 11-slice 画的边框全被清掉、只剩箭头(皇上反馈"其他边框全没了";实测整块只剩 2700 像素)。
247
+ */
248
+
249
+ export default class SpriteText3 extends Mesh {
250
+ constructor(
251
+ text: string = "",
252
+ textHeight: number = 10,
253
+ color: ColorString = "#ffffff",
254
+ ) {
255
+ const shader = new ShaderMaterial({
256
+ transparent: true,
257
+ depthWrite: false,
258
+ side: DoubleSide,
259
+ uniforms: {
260
+ uTextTexture: { value: createPlaceholderTexture() },
261
+ },
262
+ vertexShader: VERTEX_SHADER,
263
+ fragmentShader: FRAGMENT_SHADER,
264
+ });
265
+ // 几何底部落在模型原点,箭头尖即 billboard 锚点
266
+ super(new PlaneGeometry(1, 1).translate(0, 0.5, 0), shader);
267
+
268
+ this._shader = shader;
269
+ this._text = `${text}`;
270
+ this._textHeight = textHeight;
271
+ this._color = color;
272
+
273
+ this._loadBg();
274
+ this._genText();
275
+ }
276
+
277
+ /** 自定义着色器材质(类型收窄用) */
278
+ private _shader: ShaderMaterial;
279
+ /** 气泡+文字纹理(_genText 重建) */
280
+ private _textTexture: Texture | null = null;
281
+ /** 绘制用离屏 canvas */
282
+ private _textCanvas: HTMLCanvasElement = document.createElement("canvas");
283
+ /** 气泡背景图源图(加载完成后在 canvas 上做 11-slice) */
284
+ private _bgImage: HTMLImageElement | null = null;
285
+ // 箭头直接用源图原始 cell 绘制(见下方 [9] 段),无需离屏预生成。
286
+
287
+ // ========== 用户可调属性 ==========
288
+ _text: string = "";
289
+ _textHeight: number = 10;
290
+ _color: ColorString = "#ffffff";
291
+ _fontFace: string = "Arial";
292
+ _fontSize: number = 90;
293
+ _fontWeight: string = "normal";
294
+ _padding: number = 8;
295
+ /**
296
+ * 边框带边长(画布像素,4 角 + R0/R2 行高 + C0/C4 列宽 共享)
297
+ * 源边框带 = 外辉光 42 + 圆角 38 = 82;默认 82 = 1:1 复刻原图(辉光完整保留)
298
+ */
299
+ _cornerSize: number = 82;
300
+ /**
301
+ * 箭头目标凸出高度(画布像素,尖角块[9]的箭头段在画布上的最终高度)
302
+ * 源图箭头 ARROW_SRC_H=41 像素,此属性控制缩放后的视觉大小(不可变形等比缩放)
303
+ * 默认 41 = 1:1 最锐利(原图箭头比例);调大超过 41 才开始放大变糊
304
+ */
305
+ _arrowTargetH: number = 41;
306
+ /**
307
+ * 箭头水平视觉微调偏移(像素,相对 canvas 中心;负值左移、正值右移)
308
+ * 源图箭头中心 x=471 相对气泡中心 461 偏右约 10px;尖角块以【箭头】为中心取块,
309
+ * 画布上块居中 → 该偏置被自然消除,默认 0;此属性供按视觉需求再做微调
310
+ */
311
+ _arrowOffsetX: number = 0;
312
+ /** 是否显示 11-slice 分割线(默认开启) */
313
+ _showGrid: boolean = false;
314
+ /** 是否显示调试描边(默认关闭,画整个 canvas 红色边框) */
315
+ _showOutline: boolean = false;
316
+
317
+ get text(): string { return this._text; }
318
+ set text(value: string) { this._text = value; this._genText(); }
319
+
320
+ get textHeight(): number { return this._textHeight; }
321
+ set textHeight(value: number) { this._textHeight = value; this._genText(); }
322
+
323
+ get color(): ColorString { return this._color; }
324
+ set color(value: ColorString) { this._color = value; this._genText(); }
325
+
326
+ get fontFace(): string { return this._fontFace; }
327
+ set fontFace(value: string) { this._fontFace = value; this._genText(); }
328
+
329
+ get fontSize(): number { return this._fontSize; }
330
+ set fontSize(value: number) { this._fontSize = value; this._genText(); }
331
+
332
+ get fontWeight(): string { return this._fontWeight; }
333
+ set fontWeight(value: string) { this._fontWeight = value; this._genText(); }
334
+
335
+ get padding(): number { return this._padding; }
336
+ set padding(value: number) { this._padding = value; this._genText(); }
337
+
338
+ get cornerSize(): number { return this._cornerSize; }
339
+ set cornerSize(value: number) { this._cornerSize = Math.max(0, value); this._genText(); }
340
+
341
+ get arrowTargetH(): number { return this._arrowTargetH; }
342
+ set arrowTargetH(value: number) { this._arrowTargetH = Math.max(0, value); this._genText(); }
343
+
344
+ get arrowOffsetX(): number { return this._arrowOffsetX; }
345
+ set arrowOffsetX(value: number) { this._arrowOffsetX = value; this._genText(); }
346
+
347
+ get showOutline(): boolean { return this._showOutline; }
348
+ set showOutline(value: boolean) { this._showOutline = value; this._genText(); }
349
+
350
+ get showGrid(): boolean { return this._showGrid; }
351
+ set showGrid(value: boolean) { this._showGrid = value; this._genText(); }
352
+
353
+ /** 异步载入背景图源图(base64 data URL),加载完成后触发 11-slice 重绘 */
354
+ private _loadBg(): void {
355
+ const img = new Image();
356
+ img.onload = () => {
357
+ this._bgImage = img;
358
+ this._genText();
359
+ };
360
+ img.src = defaultBg;
361
+ }
362
+
363
+ /**
364
+ * 重新生成 canvas 纹理并同步 sprite 世界尺寸。
365
+ * 主体流程:计算文字/气泡尺寸 → 计算 11 切片的目标位置/大小 → 依次 drawImage 11 个部件
366
+ * → 画 showGrid 分割线 → 画 showOutline 边框 → 画文字 → 更新 Texture。
367
+ */
368
+ private _genText(): void {
369
+ const lines = this._text.split("\n");
370
+ const ctx = this._textCanvas.getContext("2d");
371
+ if (!ctx) return;
372
+
373
+ const font = `${this._fontWeight} ${this._fontSize}px ${this._fontFace}`;
374
+ ctx.font = font;
375
+
376
+ // 逐行度量真实墨迹尺寸
377
+ const metrics = lines.map((line) => {
378
+ const m = ctx.measureText(line);
379
+ const ascent = m.actualBoundingBoxAscent ?? this._fontSize * 0.8;
380
+ const descent = m.actualBoundingBoxDescent ?? this._fontSize * 0.2;
381
+ return { width: m.width, ascent, descent };
382
+ });
383
+ const textWpx = Math.max(...metrics.map((m) => m.width), 1);
384
+ const lineH = this._fontSize;
385
+ const blockHpx =
386
+ lines.length <= 1
387
+ ? metrics[0].ascent + metrics[0].descent
388
+ : metrics[0].ascent + metrics[metrics.length - 1].descent + (lines.length - 1) * lineH;
389
+ // 文本高度下限:单行不低于字号高度,多行不低于「行数×字号」,
390
+ // 避免扁字形(如"一")真实墨迹高度过小、把气泡压扁而包不住文字。
391
+ const textHpx = Math.max(blockHpx, this._fontSize * lines.length, 1);
392
+
393
+ // 世界/像素换算
394
+ const pxToWorld = this._textHeight / this._fontSize;
395
+ const padX = this._padding;
396
+ const padY = this._padding;
397
+ // 文字区在画布上的尺寸(像素,不含 4 角 + 上下/左右连接带)
398
+ const textAreaW = textWpx + 2 * padX;
399
+ const textAreaH = textHpx + 2 * padY;
400
+
401
+ // 气泡主体尺寸:文字区 + 上下/左右各 cornerSize 像素的边框
402
+ const cornerSize = this._cornerSize;
403
+ const bodyWc = Math.max(Math.round(textAreaW) + 2 * cornerSize, 2 * cornerSize + 2);
404
+ const bodyHc = Math.max(Math.round(textAreaH) + 2 * cornerSize, 2 * cornerSize + 2);
405
+
406
+ // 尖角块参数([9] 下中尖角)
407
+ // 尖角块在画布上等比缩放(不可变形),缩放比由 arrowTargetH 决定
408
+ const tipScale = this._arrowTargetH / ARROW_SRC_H;
409
+ const tipDstW = TIP_SRC_W * tipScale;
410
+ const tipDstH = TIP_SRC_H * tipScale;
411
+ // 尖角块位置:以箭头为中心水平居中 + 视觉微调
412
+ const tipDstX = bodyWc / 2 - tipDstW / 2 + this._arrowOffsetX;
413
+ // 不透明本体(描边)底在画布上的 y:底边带(含辉光尾)内,辉光尾占 GLOW_TAIL/SRC_CORNER 比例
414
+ const solidBottomDst = bodyHc - cornerSize * (GLOW_TAIL / SRC_CORNER);
415
+ // 尖角块顶 = 描边底的【下一行】(箭头紧贴下边框下方,绝不与下边框重叠哪怕 1px)
416
+ const tipDstY = solidBottomDst + 1;
417
+ const arrowTipDst = tipDstY + tipDstH;
418
+ // 画布总高:默认箭头尖埋在底部辉光带内(与原图一致);arrowTargetH 调大超出辉光带时加高
419
+ const canvasW = bodyWc;
420
+ const canvasH = Math.max(bodyHc, Math.ceil(arrowTipDst));
421
+
422
+ this._textCanvas.width = canvasW;
423
+ this._textCanvas.height = canvasH;
424
+
425
+ // resize 会清空画布,重设状态
426
+ ctx.font = font;
427
+ ctx.clearRect(0, 0, canvasW, canvasH);
428
+
429
+ // ===== 11-slice 背景绘制(顺序:底边带 → 中间行 → 上边带 → 顶角 → 尖角叠加)=====
430
+ if (this._bgImage && this._bgImage.naturalWidth > 0) {
431
+ const img = this._bgImage;
432
+ // 箭头 [9] 直接用原始源图 cell 绘制(见下方 [9] 段),不再预生成离屏 canvas、不再染同色。
433
+ // 拉伸块的源区段(标准 9-patch:一律取「从一角块边界到另一角块边界」的完整连续区段,
434
+ // 使相邻块在拼接处的源像素相邻/对称 → 无接缝)。
435
+ // ⚠️ 2026-09-14 修(皇上反馈「左上圆角正常、其他 3 个圆角拼接不对 + 箭头两侧可见接缝」):
436
+ // 原实现所有拉伸块都只取源图【左上角内侧】固定 82px 的一小段来拉伸,导致除「左上角」外的
437
+ // 各拼接边两侧源像素不相邻:中间行底端源 y163 紧邻底边带顶源 y310(跨 147px,而源图本体
438
+ // 底部 y≈300 起有明显内发光变亮 (9,27,58)→(12,32,74))→ 画布 y=bodyHc-cornerSize 处
439
+ // 横向台阶;上边带右端源 x163 紧邻右上角块源 x840 → 画布 x=bodyWc-cornerSize 处纵向台阶;
440
+ // 底边带中心源 x163↔x758 与尖角块源 x441/501 亦不连续 → 箭头两侧接缝。
441
+ // 改为整段取源后,所有相邻边界源像素连续(y309↔y310、x839↔x840、x440↔x441、x500↔x501)。
442
+ const srcMidW = SRC_BODY_RIGHT - SRC_BODY_LEFT - 2 * SRC_CORNER; // 上/中带源宽 = 922-164 = 758
443
+ const srcMidH = SRC_BODY_BOTTOM - SRC_BODY_TOP - 2 * SRC_CORNER; // 左/中/右带源高 = 392-164 = 228
444
+ const srcBottomLeftW = TIP_SRC_X - (SRC_BODY_LEFT + SRC_CORNER); // 左中连接边源宽 = 441-82 = 359
445
+ const srcBottomRightW =
446
+ SRC_BODY_RIGHT - SRC_CORNER - (TIP_SRC_X + TIP_SRC_W); // 中右连接边源宽 = 840-501 = 339
447
+
448
+ const draw = (
449
+ sx: number, sy: number, sw: number, sh: number,
450
+ dx: number, dy: number, dw: number, dh: number,
451
+ ): void => {
452
+ if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) return;
453
+ ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
454
+ };
455
+
456
+ // ----- 底边带(R2 行:Y = bodyHc - cornerSize ~ bodyHc)-----
457
+ // ⚠️ 两条连接边【横跨整个底边带】(画到 canvas 水平中心),尖角块[9]最后叠加在中央之上,
458
+ // 底边带中间不留空隙(2026-09-02 踩坑:若连接边止步尖角块边缘,尖角块一旦异常
459
+ // 中间就会露出透明洞)。
460
+ // ⚠️ 但取样源端点必须【对齐尖角块左右缘】(441 / 501):这样连接边在箭头两侧与尖角块
461
+ // 的源像素连续(箭头两侧底边辉光镜像对称,x441=x501),消除「箭头两侧接缝」。
462
+ // [7] 左下角:源 (SRC_BODY_LEFT, SRC_BODY_BOTTOM - SRC_CORNER, SRC_CORNER, SRC_CORNER)
463
+ draw(
464
+ SRC_BODY_LEFT, SRC_BODY_BOTTOM - SRC_CORNER, SRC_CORNER, SRC_CORNER,
465
+ 0, bodyHc - cornerSize, cornerSize, cornerSize,
466
+ );
467
+ // [8] 左中连接边:源 x[82, 441)(右端紧贴尖角块左缘)→ 目标 (cornerSize ~ canvas 水平中心)
468
+ draw(
469
+ SRC_BODY_LEFT + SRC_CORNER, SRC_BODY_BOTTOM - SRC_CORNER, srcBottomLeftW, SRC_CORNER,
470
+ cornerSize, bodyHc - cornerSize, bodyWc / 2 - cornerSize, cornerSize,
471
+ );
472
+ // [10] 中右连接边:源 x[501, 840)(左端紧贴尖角块右缘)→ 目标 (canvas 水平中心 ~ bodyWc-cornerSize)
473
+ draw(
474
+ TIP_SRC_X + TIP_SRC_W, SRC_BODY_BOTTOM - SRC_CORNER, srcBottomRightW, SRC_CORNER,
475
+ bodyWc / 2, bodyHc - cornerSize, bodyWc / 2 - cornerSize, cornerSize,
476
+ );
477
+ // [11] 右下角:源 (SRC_BODY_RIGHT - SRC_CORNER, SRC_BODY_BOTTOM - SRC_CORNER, ...)
478
+ draw(
479
+ SRC_BODY_RIGHT - SRC_CORNER, SRC_BODY_BOTTOM - SRC_CORNER, SRC_CORNER, SRC_CORNER,
480
+ bodyWc - cornerSize, bodyHc - cornerSize, cornerSize, cornerSize,
481
+ );
482
+
483
+ // ----- 中间行(R1 行:Y = cornerSize ~ bodyHc - cornerSize)-----
484
+ // 源一律取 y[82, 310)(整段中间高):底端 y309 与底边带顶端 y310 连续 → 无横向台阶。
485
+ // [4] 左边:纵向拉伸
486
+ draw(
487
+ SRC_BODY_LEFT, SRC_BODY_TOP + SRC_CORNER, SRC_CORNER, srcMidH,
488
+ 0, cornerSize, cornerSize, bodyHc - 2 * cornerSize,
489
+ );
490
+ // [5] 中间:双向拉伸
491
+ draw(
492
+ SRC_BODY_LEFT + SRC_CORNER, SRC_BODY_TOP + SRC_CORNER, srcMidW, srcMidH,
493
+ cornerSize, cornerSize, bodyWc - 2 * cornerSize, bodyHc - 2 * cornerSize,
494
+ );
495
+ // [6] 右边:纵向拉伸
496
+ draw(
497
+ SRC_BODY_RIGHT - SRC_CORNER, SRC_BODY_TOP + SRC_CORNER, SRC_CORNER, srcMidH,
498
+ bodyWc - cornerSize, cornerSize, cornerSize, bodyHc - 2 * cornerSize,
499
+ );
500
+
501
+ // ----- 顶边带(R0 行:Y = 0 ~ cornerSize)-----
502
+ // [1] 左上角
503
+ draw(
504
+ SRC_BODY_LEFT, SRC_BODY_TOP, SRC_CORNER, SRC_CORNER,
505
+ 0, 0, cornerSize, cornerSize,
506
+ );
507
+ // [2] 上边:横向拉伸,源取 x[82, 840)(整段中间宽):右端 x839 与右上角块 x840 连续 → 无纵向台阶
508
+ draw(
509
+ SRC_BODY_LEFT + SRC_CORNER, SRC_BODY_TOP, srcMidW, SRC_CORNER,
510
+ cornerSize, 0, bodyWc - 2 * cornerSize, cornerSize,
511
+ );
512
+ // [3] 右上角
513
+ draw(
514
+ SRC_BODY_RIGHT - SRC_CORNER, SRC_BODY_TOP, SRC_CORNER, SRC_CORNER,
515
+ bodyWc - cornerSize, 0, cornerSize, cornerSize,
516
+ );
517
+
518
+ // ----- 尖角块 [9](最后画,叠加在底边带中央)-----
519
+ // 方案 = 「先 clearRect 局部清空 + source-over 画原始 cell」(等价"局部 copy"),一次解决三个约束:
520
+ // · 无缝:先清掉 [9] 矩形内的带子,箭头边距 α 就不会与带子 α 相加 → 与左右带子逐像素一致。
521
+ // ⚠️ 若直接 source-over,边距 α 与带子 α 相加(32+33≈60) → 又现台阶(皇上反馈的"接缝")。
522
+ // · 清晰:箭身/尖点=亮青高透明(α≈248) → 可见。
523
+ // ⚠️ 若用 source-atop 会把箭头 α 压到带子的≈43 → 整块与带子同透明 → 看不见(皇上反馈"看不清楚")。
524
+ // · 不破坏其它块:clearRect 只清 [9] 矩形(实测仅 1144 像素差异、全落在该矩形内),
525
+ // 绝不动 11-slice 已画好的边框。
526
+ // ⚠️ 绝不可用 copy:canvas 的 copy 会【清空整个画布、只保留本次绘制】→ 边框全没(皇上反馈"其他边框全没了")。
527
+ ctx.clearRect(tipDstX, tipDstY, tipDstW, tipDstH);
528
+ draw(TIP_SRC_X, TIP_SRC_Y, TIP_SRC_W, TIP_SRC_H, tipDstX, tipDstY, tipDstW, tipDstH);
529
+ }
530
+
531
+ // ===== 调试分割线(11 块边界 + 文字区边界)=====
532
+ if (this._showGrid) {
533
+ ctx.save();
534
+ ctx.strokeStyle = "rgba(255,0,0,0.85)";
535
+ ctx.lineWidth = 1;
536
+ ctx.beginPath();
537
+ // 4 角方框(不可变形区域可视化)
538
+ ctx.strokeRect(0, 0, cornerSize, cornerSize);
539
+ ctx.strokeRect(bodyWc - cornerSize, 0, cornerSize, cornerSize);
540
+ ctx.strokeRect(0, bodyHc - cornerSize, cornerSize, cornerSize);
541
+ ctx.strokeRect(bodyWc - cornerSize, bodyHc - cornerSize, cornerSize, cornerSize);
542
+ // 上/下/左/右可拉伸带边界
543
+ ctx.moveTo(cornerSize, 0); ctx.lineTo(cornerSize, cornerSize);
544
+ ctx.moveTo(bodyWc - cornerSize, 0); ctx.lineTo(bodyWc - cornerSize, cornerSize);
545
+ ctx.moveTo(0, cornerSize); ctx.lineTo(cornerSize, cornerSize);
546
+ ctx.moveTo(0, bodyHc - cornerSize); ctx.lineTo(cornerSize, bodyHc - cornerSize);
547
+ // 中间行上下边界
548
+ ctx.moveTo(cornerSize, cornerSize); ctx.lineTo(bodyWc - cornerSize, cornerSize);
549
+ ctx.moveTo(cornerSize, bodyHc - cornerSize); ctx.lineTo(bodyWc - cornerSize, bodyHc - cornerSize);
550
+ // 左右可拉伸带与中间区分界
551
+ ctx.moveTo(cornerSize, cornerSize); ctx.lineTo(cornerSize, bodyHc - cornerSize);
552
+ ctx.moveTo(bodyWc - cornerSize, cornerSize); ctx.lineTo(bodyWc - cornerSize, bodyHc - cornerSize);
553
+ // 尖角块轮廓(叠加在底边带中央,含下方凸出的箭头段)
554
+ ctx.strokeRect(tipDstX, tipDstY, tipDstW, tipDstH);
555
+ ctx.stroke();
556
+ ctx.restore();
557
+ }
558
+
559
+ // 调试描边:整个 canvas 红色边框
560
+ if (this._showOutline) {
561
+ ctx.save();
562
+ ctx.strokeStyle = "rgba(255,0,0,0.85)";
563
+ ctx.lineWidth = 2;
564
+ ctx.strokeRect(1, 1, canvasW - 2, canvasH - 2);
565
+ ctx.restore();
566
+ }
567
+
568
+ // ===== 绘制文字 =====
569
+ // 文字块居中在 body 主体内(文字区中心 = (bodyWc/2, cornerSize + textAreaH/2))
570
+ ctx.save();
571
+ ctx.fillStyle = this._color;
572
+ ctx.textAlign = "center";
573
+ ctx.textBaseline = "alphabetic";
574
+ const cx = bodyWc / 2;
575
+ const textCenterY = cornerSize + (bodyHc - 2 * cornerSize) / 2;
576
+ const firstAscent = metrics[0].ascent;
577
+ const lastDescent = metrics[metrics.length - 1].descent;
578
+ const y0 = textCenterY - (lines.length - 1) * lineH / 2 + (firstAscent - lastDescent) / 2;
579
+ lines.forEach((line, index) => {
580
+ const y = y0 + index * lineH;
581
+ ctx.fillText(line, cx, y);
582
+ });
583
+ ctx.restore();
584
+
585
+ // 更新纹理
586
+ this._textTexture?.dispose();
587
+ const texture = new Texture(this._textCanvas);
588
+ texture.colorSpace = SRGBColorSpace;
589
+ texture.minFilter = LinearFilter;
590
+ texture.magFilter = LinearFilter;
591
+ texture.needsUpdate = true;
592
+ this._textTexture = texture;
593
+ this._shader.uniforms.uTextTexture.value = texture;
594
+
595
+ // 同步 sprite 世界尺寸
596
+ // 世界宽 = 画布宽 × pxToWorld;世界高 = 画布高 × pxToWorld(箭头凸出随之缩放)
597
+ this.scale.set(canvasW * pxToWorld, canvasH * pxToWorld, 1);
598
+ }
599
+ }
@@ -1,7 +1,9 @@
1
1
  import { Sprite } from "three";
2
+ type HexColorString = `#${string}`;
2
3
  export default class SpriteWithHtml extends Sprite {
3
- constructor(html?: string, backgroundColor?: `#${string}`);
4
+ constructor(html?: string, backgroundColor?: HexColorString);
4
5
  visible: boolean;
5
6
  div: HTMLDivElement;
6
- setHtml(html?: string, backgroundColor?: `#${string}`): Promise<void>;
7
+ setHtml(html?: string, backgroundColor?: HexColorString): Promise<void>;
7
8
  }
9
+ export {};
@@ -0,0 +1,50 @@
1
+ /*
2
+ * @Author: xiaosihan
3
+ * @Date: 2023-04-15 11:26:32
4
+ * @Last Modified by: xiaosihan
5
+ * @Last Modified time: 2023-04-15 12:38:09
6
+ */
7
+ import { snapdom } from "@zumer/snapdom";
8
+ import { Texture, Sprite } from "three";
9
+ import threeLoader from "./threeLoader";
10
+ import type { SpriteWithHtmlEventMap } from "./EventMap";
11
+
12
+ type HexColorString = `#${string}`;
13
+
14
+ // html 文本面板
15
+ export default class SpriteWithHtml extends Sprite {
16
+ constructor(html: string = "", backgroundColor: HexColorString = "#00000000") {
17
+ super();
18
+ this.setHtml(html, backgroundColor);
19
+ }
20
+
21
+ visible: boolean = false;
22
+ div: HTMLDivElement = (() => {
23
+ const div = document.createElement("div");
24
+ div.style.position = "fixed";
25
+ div.style.top = "100%";
26
+ div.style.left = "100%";
27
+ div.style.pointerEvents = "none";
28
+ div.style.userSelect = "none";
29
+ div.style.lineHeight = "1";
30
+ div.style.whiteSpace = "nowrap";
31
+ return div;
32
+ })();
33
+
34
+ async setHtml(html: string = "", backgroundColor: HexColorString = "#00000000"): Promise<void> {
35
+ this.div.innerHTML = html;
36
+ document.body.appendChild(this.div);
37
+ const img = await snapdom.toPng(this.div, { backgroundColor });
38
+ this.scale.set(img.naturalWidth, img.naturalHeight, 1);
39
+ const texture = new Texture(img);
40
+ texture.needsUpdate = true;
41
+ this.material.map = texture;
42
+ this.material.map.anisotropy = threeLoader.anisotropy;
43
+ this.material.alphaTest = 0.1;
44
+ this.material.needsUpdate = true;
45
+ await new Promise(resolve => requestAnimationFrame(resolve));
46
+ this.visible = true;
47
+ this.div.remove();
48
+ this.dispatchEvent({ type: "loaded" } as never);
49
+ }
50
+ }