three-base 2.0.137 → 2.0.142

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 (307) hide show
  1. package/DirectionalLight.d.ts +6 -6
  2. package/DirectionalLight.js +68 -68
  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.d.ts +18 -18
  8. package/EventMesh.js +59 -59
  9. package/EventMesh.ts +60 -0
  10. package/ExtrudeMesh.d.ts +112 -112
  11. package/ExtrudeMesh.js +342 -366
  12. package/ExtrudeMesh.ts +553 -0
  13. package/FPScontrol.d.ts +511 -78
  14. package/FPScontrol.js +1799 -617
  15. package/FPScontrol.ts +1855 -0
  16. package/Fire/Fire.d.ts +28 -28
  17. package/Fire/Fire.js +80 -79
  18. package/Fire/Fire.ts +89 -0
  19. package/Fire/{FireGromtry.d.ts → FireGeometry.d.ts} +9 -9
  20. package/Fire/{FireGromtry.js → FireGeometry.js} +40 -40
  21. package/Fire/FireGeometry.ts +40 -0
  22. package/Fire/FireMaterial.d.ts +28 -28
  23. package/Fire/FireMaterial.js +32 -32
  24. package/Fire/FireMaterial.ts +89 -0
  25. package/Fire/index.d.ts +2 -1
  26. package/Fire/index.js +2 -1
  27. package/Fire/index.ts +2 -0
  28. package/GLTFDracoLoader.d.ts +71 -0
  29. package/GLTFDracoLoader.js +266 -0
  30. package/GLTFDracoLoader.ts +430 -0
  31. package/Grass/Grass.d.ts +8 -8
  32. package/Grass/Grass.js +38 -38
  33. package/Grass/Grass.ts +38 -0
  34. package/Grass/index.d.ts +2 -1
  35. package/Grass/index.js +2 -1
  36. package/Grass/index.ts +2 -0
  37. package/Ground1/Ground1.d.ts +10 -11
  38. package/Ground1/Ground1.js +95 -91
  39. package/Ground1/Ground1.ts +114 -0
  40. package/Ground1/index.d.ts +1 -1
  41. package/Ground1/index.js +1 -1
  42. package/Ground1/index.ts +1 -0
  43. package/MeshBase.d.ts +36 -35
  44. package/MeshBase.js +247 -223
  45. package/MeshBase.ts +253 -0
  46. package/Mirror.d.ts +25 -25
  47. package/Mirror.js +143 -139
  48. package/Mirror.ts +203 -0
  49. package/MktMesh/MktMesh.d.ts +291 -0
  50. package/MktMesh/MktMesh.js +1045 -0
  51. package/MktMesh/MktMesh.ts +1075 -0
  52. package/MktMesh/assetUrl.d.ts +35 -0
  53. package/MktMesh/assetUrl.js +69 -0
  54. package/MktMesh/assetUrl.ts +69 -0
  55. package/MktMesh/mktMappingShader.d.ts +12 -0
  56. package/MktMesh/mktMappingShader.js +139 -0
  57. package/MktMesh/mktMappingShader.ts +139 -0
  58. package/MktMesh/mktShader.d.ts +263 -0
  59. package/MktMesh/mktShader.js +1248 -0
  60. package/MktMesh/mktShader.ts +1344 -0
  61. package/MktMesh/panoBitmapTexture.d.ts +25 -0
  62. package/MktMesh/panoBitmapTexture.js +52 -0
  63. package/MktMesh/panoBitmapTexture.ts +54 -0
  64. package/MktMesh/panoSlotBind.d.ts +29 -0
  65. package/MktMesh/panoSlotBind.js +106 -0
  66. package/MktMesh/panoSlotBind.ts +153 -0
  67. package/MktMesh/panoSlotRoster.d.ts +22 -0
  68. package/MktMesh/panoSlotRoster.js +169 -0
  69. package/MktMesh/panoSlotRoster.ts +168 -0
  70. package/MktMesh/panoTileStitcher.d.ts +119 -0
  71. package/MktMesh/panoTileStitcher.js +533 -0
  72. package/MktMesh/panoTileStitcher.ts +616 -0
  73. package/MktMesh/types.d.ts +75 -0
  74. package/MktMesh/types.js +1 -0
  75. package/MktMesh/types.ts +87 -0
  76. package/MktMesh/wallCapTessellate.d.ts +33 -0
  77. package/MktMesh/wallCapTessellate.js +97 -0
  78. package/MktMesh/wallCapTessellate.ts +68 -0
  79. package/PipeMesh/PathGeometry.d.ts +44 -44
  80. package/PipeMesh/PathGeometry.js +307 -300
  81. package/PipeMesh/PathGeometry.ts +343 -0
  82. package/PipeMesh/PathPoint.d.ts +17 -17
  83. package/PipeMesh/PathPoint.js +31 -31
  84. package/PipeMesh/PathPoint.ts +36 -0
  85. package/PipeMesh/PathPointList.d.ts +31 -31
  86. package/PipeMesh/PathPointList.js +209 -208
  87. package/PipeMesh/PathPointList.ts +227 -0
  88. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  89. package/PipeMesh/PathTubeGeometry.js +130 -130
  90. package/PipeMesh/PathTubeGeometry.ts +146 -0
  91. package/PipeMesh/PipeMesh.d.ts +25 -25
  92. package/PipeMesh/PipeMesh.js +63 -63
  93. package/PipeMesh/PipeMesh.ts +76 -0
  94. package/PipeMesh/index.d.ts +1 -1
  95. package/PipeMesh/index.js +1 -1
  96. package/PipeMesh/index.ts +1 -0
  97. package/PlaneModal1/PlaneModal1.d.ts +34 -31
  98. package/PlaneModal1/PlaneModal1.js +109 -105
  99. package/PlaneModal1/PlaneModal1.ts +138 -0
  100. package/PlaneModal1/index.d.ts +1 -1
  101. package/PlaneModal1/index.js +1 -1
  102. package/PlaneModal1/index.ts +1 -0
  103. package/PlaneText.d.ts +27 -39
  104. package/PlaneText.js +35 -30
  105. package/PlaneText.ts +55 -0
  106. package/PlaneWithHtml.d.ts +13 -11
  107. package/PlaneWithHtml.js +64 -63
  108. package/PlaneWithHtml.ts +70 -0
  109. package/PointLight/PointLight.d.ts +13 -14
  110. package/PointLight/PointLight.js +64 -70
  111. package/PointLight/PointLight.ts +67 -0
  112. package/PointLight/index.d.ts +1 -1
  113. package/PointLight/index.js +1 -1
  114. package/PointLight/index.ts +1 -0
  115. package/README.md +110 -79
  116. package/Sky/Sky.d.ts +9 -9
  117. package/Sky/Sky.js +22 -22
  118. package/Sky/Sky.ts +22 -0
  119. package/Sky/index.d.ts +1 -1
  120. package/Sky/index.js +1 -1
  121. package/Sky/index.ts +1 -0
  122. package/SpotLight.d.ts +6 -6
  123. package/SpotLight.js +72 -72
  124. package/SpotLight.ts +74 -0
  125. package/SpriteModal1/SpriteModal1.d.ts +33 -30
  126. package/SpriteModal1/SpriteModal1.js +116 -110
  127. package/SpriteModal1/SpriteModal1.ts +146 -0
  128. package/SpriteModal1/index.d.ts +1 -1
  129. package/SpriteModal1/index.js +1 -1
  130. package/SpriteModal1/index.ts +1 -0
  131. package/SpriteText.d.ts +46 -45
  132. package/SpriteText.js +176 -163
  133. package/SpriteText.ts +205 -0
  134. package/SpriteText2.d.ts +48 -47
  135. package/SpriteText2.js +223 -198
  136. package/SpriteText2.ts +258 -0
  137. package/SpriteText3.d.ts +86 -0
  138. package/SpriteText3.js +519 -0
  139. package/SpriteText3.ts +599 -0
  140. package/SpriteWithHtml.d.ts +9 -7
  141. package/SpriteWithHtml.js +44 -44
  142. package/SpriteWithHtml.ts +50 -0
  143. package/Text3D/Text3D.d.ts +20 -20
  144. package/Text3D/Text3D.js +58 -62
  145. package/Text3D/Text3D.ts +77 -0
  146. package/Text3D/fontController.d.ts +60 -59
  147. package/Text3D/fontController.js +166 -199
  148. package/Text3D/fontController.ts +209 -0
  149. package/Text3D/index.d.ts +1 -1
  150. package/Text3D/index.js +1 -1
  151. package/Text3D/index.ts +1 -0
  152. package/ThreeBase.d.ts +248 -111
  153. package/ThreeBase.js +1202 -656
  154. package/ThreeBase.ts +1314 -0
  155. package/ThreeMouse.d.ts +54 -40
  156. package/ThreeMouse.js +453 -407
  157. package/ThreeMouse.ts +472 -0
  158. package/Transition.d.ts +45 -40
  159. package/Transition.js +209 -210
  160. package/Transition.ts +242 -0
  161. package/Water/Water.d.ts +5 -5
  162. package/Water/Water.js +44 -26
  163. package/Water/Water.ts +45 -0
  164. package/Water/index.d.ts +2 -0
  165. package/Water/index.js +2 -0
  166. package/Water/index.ts +2 -0
  167. package/WaterFlow/FloatingDuck.d.ts +88 -0
  168. package/WaterFlow/FloatingDuck.js +307 -0
  169. package/WaterFlow/FloatingDuck.ts +445 -0
  170. package/WaterFlow/GPUFluidSimulation.d.ts +56 -0
  171. package/WaterFlow/GPUFluidSimulation.js +785 -0
  172. package/WaterFlow/GPUFluidSimulation.ts +827 -0
  173. package/WaterFlow/ReservoirSystem.d.ts +106 -0
  174. package/WaterFlow/ReservoirSystem.js +621 -0
  175. package/WaterFlow/ReservoirSystem.ts +754 -0
  176. package/WaterFlow/SPHField.d.ts +35 -0
  177. package/WaterFlow/SPHField.js +204 -0
  178. package/WaterFlow/SPHField.ts +228 -0
  179. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -0
  180. package/WaterFlow/ScreenSpaceFluidPass.js +736 -0
  181. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -0
  182. package/WaterFlow/ShallowWaterSurface.d.ts +45 -0
  183. package/WaterFlow/ShallowWaterSurface.js +441 -0
  184. package/WaterFlow/ShallowWaterSurface.ts +510 -0
  185. package/WaterFlow/WaterFlow.d.ts +87 -0
  186. package/WaterFlow/WaterFlow.js +171 -0
  187. package/WaterFlow/WaterFlow.ts +219 -0
  188. package/WaterFlow/types.d.ts +161 -0
  189. package/WaterFlow/types.js +1 -0
  190. package/WaterFlow/types.ts +189 -0
  191. package/WaterFlow/waterFlowPass.d.ts +3 -0
  192. package/WaterFlow/waterFlowPass.js +20 -0
  193. package/WaterFlow/waterFlowPass.ts +40 -0
  194. package/collisionOBB.d.ts +42 -0
  195. package/collisionOBB.js +112 -0
  196. package/collisionOBB.ts +108 -0
  197. package/declare.d.ts +23 -165
  198. package/index.d.ts +1 -1
  199. package/index.js +1 -2
  200. package/index.ts +1 -0
  201. package/instanced/InstancedBatch.d.ts +57 -0
  202. package/instanced/InstancedBatch.js +348 -0
  203. package/instanced/InstancedBatch.ts +406 -0
  204. package/instanced/InstancedChild.d.ts +174 -0
  205. package/instanced/InstancedChild.js +264 -0
  206. package/instanced/InstancedChild.ts +327 -0
  207. package/instanced/InstancedGroup.d.ts +43 -0
  208. package/instanced/InstancedGroup.js +126 -0
  209. package/instanced/InstancedGroup.ts +137 -0
  210. package/{InstancedMeshBase.d.ts → instanced/InstancedMeshBase.d.ts} +38 -39
  211. package/{InstancedMeshBase.js → instanced/InstancedMeshBase.js} +282 -293
  212. package/instanced/InstancedMeshBase.ts +292 -0
  213. package/instanced/InstancedTemplate.d.ts +46 -0
  214. package/instanced/InstancedTemplate.js +125 -0
  215. package/instanced/InstancedTemplate.ts +188 -0
  216. package/keyControls.d.ts +30 -0
  217. package/{keyContorls.js → keyControls.js} +101 -98
  218. package/keyControls.ts +107 -0
  219. package/lensflare/LensFlare.d.ts +39 -0
  220. package/lensflare/LensFlare.js +373 -0
  221. package/lensflare/LensFlare.ts +448 -0
  222. package/lensflare/lensflare0.png +0 -0
  223. package/lensflare/lensflare3.png +0 -0
  224. package/lensflare/lensflare_custom.png +0 -0
  225. package/lensflare/lensflare_hex_ghost.png +0 -0
  226. package/lensflare/lensflare_hex_main.png +0 -0
  227. package/package.json +99 -25
  228. package/pass/bloom_pass.d.ts +2 -2
  229. package/pass/bloom_pass.js +188 -101
  230. package/pass/bloom_pass.ts +243 -0
  231. package/pass/cover_pass.d.ts +2 -2
  232. package/pass/cover_pass.js +122 -99
  233. package/pass/cover_pass.ts +182 -0
  234. package/pass/debugDepthState.d.ts +20 -0
  235. package/pass/debugDepthState.js +9 -0
  236. package/pass/debugDepthState.ts +22 -0
  237. package/pass/depthTestBias.d.ts +12 -0
  238. package/pass/depthTestBias.js +12 -0
  239. package/pass/depthTestBias.ts +12 -0
  240. package/pass/effectUserData.d.ts +27 -0
  241. package/pass/effectUserData.js +1 -0
  242. package/pass/effectUserData.ts +46 -0
  243. package/pass/frosted_glass_pass.d.ts +2 -2
  244. package/pass/frosted_glass_pass.js +167 -186
  245. package/pass/frosted_glass_pass.ts +288 -0
  246. package/pass/fsQuad.d.ts +9 -9
  247. package/pass/fsQuad.js +28 -28
  248. package/pass/fsQuad.ts +34 -0
  249. package/pass/glow_pass.d.ts +2 -2
  250. package/pass/glow_pass.js +449 -233
  251. package/pass/glow_pass.ts +654 -0
  252. package/pass/screen_bloom_pass.d.ts +11 -0
  253. package/pass/screen_bloom_pass.js +235 -0
  254. package/pass/screen_bloom_pass.ts +263 -0
  255. package/pass/ssao_pass.d.ts +88 -13
  256. package/pass/ssao_pass.js +802 -366
  257. package/pass/ssao_pass.ts +918 -0
  258. package/pass/stroke_pass.d.ts +2 -2
  259. package/pass/stroke_pass.js +329 -258
  260. package/pass/stroke_pass.ts +638 -0
  261. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  262. package/shaders/FXAAShaderMaterial.js +23 -66
  263. package/shaders/FXAAShaderMaterial.ts +201 -0
  264. package/shaders/baseDepthVisShader.d.ts +11 -0
  265. package/shaders/baseDepthVisShader.js +36 -0
  266. package/shaders/baseDepthVisShader.ts +36 -0
  267. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  268. package/shaders/baseTextureToRenderShader.js +55 -60
  269. package/shaders/baseTextureToRenderShader.ts +232 -0
  270. package/shaders/copyShader.d.ts +3 -3
  271. package/shaders/copyShader.js +13 -14
  272. package/shaders/copyShader.ts +32 -0
  273. package/shaders/debugDepthCompareShader.d.ts +14 -0
  274. package/shaders/debugDepthCompareShader.js +49 -0
  275. package/shaders/debugDepthCompareShader.ts +50 -0
  276. package/shaders/debugDepthJudgeShader.d.ts +18 -0
  277. package/shaders/debugDepthJudgeShader.js +52 -0
  278. package/shaders/debugDepthJudgeShader.ts +54 -0
  279. package/shaders/debugExcludedShader.d.ts +13 -0
  280. package/shaders/debugExcludedShader.js +46 -0
  281. package/shaders/debugExcludedShader.ts +48 -0
  282. package/spriteTextBg.d.ts +1 -0
  283. package/spriteTextBg.js +20 -0
  284. package/spriteTextBg.ts +20 -0
  285. package/threeLoader.d.ts +35 -34
  286. package/threeLoader.js +518 -527
  287. package/threeLoader.ts +525 -0
  288. package/threeUtils.d.ts +53 -52
  289. package/threeUtils.js +329 -330
  290. package/threeUtils.ts +373 -0
  291. package/types.d.ts +2 -0
  292. package/types.js +6 -0
  293. package/types.ts +8 -0
  294. package/ApngTexture.d.ts +0 -13
  295. package/ApngTexture.js +0 -61
  296. package/ApngTextureManage.d.ts +0 -18
  297. package/ApngTextureManage.js +0 -87
  298. package/KeyFrameAnimation.d.ts +0 -25
  299. package/KeyFrameAnimation.js +0 -78
  300. package/keyContorls.d.ts +0 -25
  301. package/lib/draco/draco_decoder.js +0 -34
  302. package/lib/draco/draco_decoder.wasm +0 -0
  303. package/lib/draco/draco_wasm_wrapper.js +0 -117
  304. package/pass/emissive_ao_pass.d.ts +0 -13
  305. package/pass/emissive_ao_pass.js +0 -370
  306. package/shaders.d.ts +0 -5
  307. package/shaders.js +0 -93
@@ -0,0 +1,25 @@
1
+ import { Texture } from "three";
2
+ /**
3
+ * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
4
+ *
5
+ * 为什么要换掉 three 的 `TextureLoader`:它只等 `img.onload`(字节到手),**从不解码** —— 解码是懒的,
6
+ * 真正碰像素的是上传那一拍的 `texImage2D(<img>)`,于是整段 JPEG 解码被拽到主线程上同步做完。
7
+ * 实测(2026-10-05,皇上线上的种子全景 11904×5952 / 解码后 270MB):
8
+ * `texImage2D(未解码的 <img>)` = **1266ms 全在主线程**;
9
+ * `createImageBitmap` 解码 = **768ms 在工作线程**(同期主线程跑完 40 个宏任务只花 6ms),
10
+ * 解完再 `texImage2D(<ImageBitmap>)` = **366ms**(这 366 是 270MB 像素搬运,跟解码无关,只有降尺寸才能压掉)。
11
+ * ⚠️ 试过 `await img.decode()` 这条路:同一张图 **90 秒没回**(超大图的异步解码在 Chrome 上不可靠)⇒ 别再走它。
12
+ *
13
+ * ⚠️ **上下翻这颗雷(实测,别照抄直觉)**:Chrome 对 `ImageBitmap` 源**忽略** `UNPACK_FLIP_Y_WEBGL`
14
+ * (`<img>` / canvas 源认,bitmap 源不认)⇒ 直接把 `<img>` 换成 bitmap 会让全景**上下颠倒**。
15
+ * 解法 = 建图时 `imageOrientation: "flipY"` 把翻转烘进位图,同时把 `texture.flipY = false`
16
+ * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
17
+ */
18
+ /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
19
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
20
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
21
+ export declare function loadPanoTexture(url: string): Promise<Texture>;
22
+ /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
23
+ * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
24
+ * 只在「这张纹理确实被丢掉」的清理路径上调用:关掉的位图再也传不上去,WebGL 上下文丢失后的重建会拿到空图。 */
25
+ export declare function releasePanoTexture(tex: Texture | null | undefined): void;
@@ -0,0 +1,52 @@
1
+ import { Texture } from "three";
2
+ /**
3
+ * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
4
+ *
5
+ * 为什么要换掉 three 的 `TextureLoader`:它只等 `img.onload`(字节到手),**从不解码** —— 解码是懒的,
6
+ * 真正碰像素的是上传那一拍的 `texImage2D(<img>)`,于是整段 JPEG 解码被拽到主线程上同步做完。
7
+ * 实测(2026-10-05,皇上线上的种子全景 11904×5952 / 解码后 270MB):
8
+ * `texImage2D(未解码的 <img>)` = **1266ms 全在主线程**;
9
+ * `createImageBitmap` 解码 = **768ms 在工作线程**(同期主线程跑完 40 个宏任务只花 6ms),
10
+ * 解完再 `texImage2D(<ImageBitmap>)` = **366ms**(这 366 是 270MB 像素搬运,跟解码无关,只有降尺寸才能压掉)。
11
+ * ⚠️ 试过 `await img.decode()` 这条路:同一张图 **90 秒没回**(超大图的异步解码在 Chrome 上不可靠)⇒ 别再走它。
12
+ *
13
+ * ⚠️ **上下翻这颗雷(实测,别照抄直觉)**:Chrome 对 `ImageBitmap` 源**忽略** `UNPACK_FLIP_Y_WEBGL`
14
+ * (`<img>` / canvas 源认,bitmap 源不认)⇒ 直接把 `<img>` 换成 bitmap 会让全景**上下颠倒**。
15
+ * 解法 = 建图时 `imageOrientation: "flipY"` 把翻转烘进位图,同时把 `texture.flipY = false`
16
+ * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
17
+ */
18
+ /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
19
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
20
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
21
+ export function loadPanoTexture(url) {
22
+ return fetch(url)
23
+ .then((res) => {
24
+ if (!res.ok)
25
+ throw new Error(`HTTP ${res.status}`);
26
+ return res.blob();
27
+ })
28
+ .then((blob) => createImageBitmap(blob, {
29
+ imageOrientation: "flipY",
30
+ premultiplyAlpha: "none",
31
+ colorSpaceConversion: "none",
32
+ }))
33
+ .then((bmp) => {
34
+ const tex = new Texture(bmp);
35
+ tex.flipY = false; // 翻转已烘进位图(见文件头)
36
+ tex.needsUpdate = true;
37
+ // `TextureLoader` 时代的 `<img>.src` 在这里没有了;「打印 shader 数据」探针回退取地址要用它。
38
+ tex.userData.url = url;
39
+ return tex;
40
+ });
41
+ }
42
+ /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
43
+ * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
44
+ * 只在「这张纹理确实被丢掉」的清理路径上调用:关掉的位图再也传不上去,WebGL 上下文丢失后的重建会拿到空图。 */
45
+ export function releasePanoTexture(tex) {
46
+ if (!tex)
47
+ return;
48
+ const bmp = tex.image;
49
+ tex.dispose();
50
+ if (bmp && typeof bmp.close === "function")
51
+ bmp.close();
52
+ }
@@ -0,0 +1,54 @@
1
+ import { Texture } from "three";
2
+
3
+ /**
4
+ * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
5
+ *
6
+ * 为什么要换掉 three 的 `TextureLoader`:它只等 `img.onload`(字节到手),**从不解码** —— 解码是懒的,
7
+ * 真正碰像素的是上传那一拍的 `texImage2D(<img>)`,于是整段 JPEG 解码被拽到主线程上同步做完。
8
+ * 实测(2026-10-05,皇上线上的种子全景 11904×5952 / 解码后 270MB):
9
+ * `texImage2D(未解码的 <img>)` = **1266ms 全在主线程**;
10
+ * `createImageBitmap` 解码 = **768ms 在工作线程**(同期主线程跑完 40 个宏任务只花 6ms),
11
+ * 解完再 `texImage2D(<ImageBitmap>)` = **366ms**(这 366 是 270MB 像素搬运,跟解码无关,只有降尺寸才能压掉)。
12
+ * ⚠️ 试过 `await img.decode()` 这条路:同一张图 **90 秒没回**(超大图的异步解码在 Chrome 上不可靠)⇒ 别再走它。
13
+ *
14
+ * ⚠️ **上下翻这颗雷(实测,别照抄直觉)**:Chrome 对 `ImageBitmap` 源**忽略** `UNPACK_FLIP_Y_WEBGL`
15
+ * (`<img>` / canvas 源认,bitmap 源不认)⇒ 直接把 `<img>` 换成 bitmap 会让全景**上下颠倒**。
16
+ * 解法 = 建图时 `imageOrientation: "flipY"` 把翻转烘进位图,同时把 `texture.flipY = false`
17
+ * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
18
+ */
19
+
20
+ /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
21
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
22
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
23
+ export function loadPanoTexture(url: string): Promise<Texture> {
24
+ return fetch(url)
25
+ .then((res) => {
26
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
27
+ return res.blob();
28
+ })
29
+ .then((blob) =>
30
+ createImageBitmap(blob, {
31
+ imageOrientation: "flipY",
32
+ premultiplyAlpha: "none",
33
+ colorSpaceConversion: "none",
34
+ })
35
+ )
36
+ .then((bmp) => {
37
+ const tex = new Texture(bmp);
38
+ tex.flipY = false; // 翻转已烘进位图(见文件头)
39
+ tex.needsUpdate = true;
40
+ // `TextureLoader` 时代的 `<img>.src` 在这里没有了;「打印 shader 数据」探针回退取地址要用它。
41
+ tex.userData.url = url;
42
+ return tex;
43
+ });
44
+ }
45
+
46
+ /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
47
+ * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
48
+ * 只在「这张纹理确实被丢掉」的清理路径上调用:关掉的位图再也传不上去,WebGL 上下文丢失后的重建会拿到空图。 */
49
+ export function releasePanoTexture(tex: Texture | null | undefined): void {
50
+ if (!tex) return;
51
+ const bmp = tex.image as ImageBitmap | undefined;
52
+ tex.dispose();
53
+ if (bmp && typeof bmp.close === "function") bmp.close();
54
+ }
@@ -0,0 +1,29 @@
1
+ import type { ShaderMaterial, Texture } from "three";
2
+ /** 取一张「已解码**且**已上传 GPU」的瓦片;未就绪要**当场**自行起载 / 排上传,然后返回 null。
3
+ * ⚠️ 语义硬约束(皇上旨 09-25「队列里的代码就是把加载好的纹理设进 shader 这一句」):
4
+ * 本回调**不许**做同步的重活(解码、上传、绑定判断都不许在 render 里现场发生),只负责「发起 + 报告就绪」。 */
5
+ export interface PanoSlotBindDeps {
6
+ peek(key: string, original: boolean): Texture | null;
7
+ /** 整槽口径(**只**给跨形态翻转那一次用):同一个点的 `RT_STITCH_SLOT_COUNT` 张一起要,缺的返回 null。
8
+ * 编辑器实现 = `_peekSlotGen`(带 `proxy:` 过渡档墓碑分支),库实现 = 逐张 `peek`;
9
+ * 「判不齐就整档不换」这个**决定**仍然只在本文件里下。 */
10
+ peekSlot(tiles: string[], original: boolean): (Texture | null)[];
11
+ /** 该槽从没成功绑过任何图时用来垫 sampler 的 1×1 占位(⛔ 空 sampler 会报错) */
12
+ placeholder: Texture;
13
+ }
14
+ /** 取(必要时建)某材质的逐格地址账本;`len` = `panoramas.length`(= 点数 × `RT_STITCH_SLOT_COUNT`)。 */
15
+ export declare function panoSlotTileKeys(mat: ShaderMaterial, len: number): string[];
16
+ /**
17
+ * 把一个「点槽」的 `RT_STITCH_SLOT_COUNT` 张瓦片换绑进 `panoramas[base .. base + RT_STITCH_SLOT_COUNT - 1]`。
18
+ *
19
+ * 返回 true = 本槽这一帧的图与点位**配好了**(调用方据此写投影矩阵);false = 沿用上一批(调用方矩阵也别动)。
20
+ *
21
+ * ⚠️ 只有一条例外必须整槽同帧:`uPanoTiled` 是**按槽一个标记**(每点一个数,不是每张一个),
22
+ * 它告诉着色器「这几个槽装的是左右两张拼合槽(1) 还是一张整图(0)」。跨形态那一次(瓦片 ↔ 整片、
23
+ * 或某点压根没切过片)若逐张换,中间几帧会拿整图按半幅采(或反之)⇒ 瞬间拉伸错位。
24
+ * 所以「目标形态 ≠ 本槽现有形态」时走整档判齐口径。
25
+ * ⚠️ 同形态内也分两条路(09-25 治本):**整槽换人**(有格正贴着另一批的真图 ⇒ 全部同帧换,判不齐一张都不动)与
26
+ * **逐张补格**(本槽空着或同一批地址还缺的格 ⇒ 到一张补一张)。
27
+ * 「这一格认的是哪张地址」记在 `panoSlotTileKeys`,换人不成就连账本都不改,下一帧重试。
28
+ */
29
+ export declare function panoSlotBind(mat: ShaderMaterial, panos: Texture[], base: number, tiles: string[], tiled: boolean, slot: number | undefined, deps: PanoSlotBindDeps): boolean;
@@ -0,0 +1,106 @@
1
+ import { RT_STITCH_SLOT_COUNT, setMktPanoTiled } from "./mktShader";
2
+ /**
3
+ * 「panoramas[i] 这一格认的是哪张瓦片地址」——逐张换绑的**账本**,按材质实例分 WeakMap 键。
4
+ * 放模块级 ⇒ 编辑器与库**共一本账**(两侧材质是不同实例,天然不串味);材质重建后自动从空账开始。
5
+ */
6
+ const slotTileKeys = new WeakMap();
7
+ /** 取(必要时建)某材质的逐格地址账本;`len` = `panoramas.length`(= 点数 × `RT_STITCH_SLOT_COUNT`)。 */
8
+ export function panoSlotTileKeys(mat, len) {
9
+ let keys = slotTileKeys.get(mat);
10
+ if (!keys) {
11
+ keys = new Array(len).fill("");
12
+ slotTileKeys.set(mat, keys);
13
+ }
14
+ return keys;
15
+ }
16
+ /**
17
+ * 把一个「点槽」的 `RT_STITCH_SLOT_COUNT` 张瓦片换绑进 `panoramas[base .. base + RT_STITCH_SLOT_COUNT - 1]`。
18
+ *
19
+ * 返回 true = 本槽这一帧的图与点位**配好了**(调用方据此写投影矩阵);false = 沿用上一批(调用方矩阵也别动)。
20
+ *
21
+ * ⚠️ 只有一条例外必须整槽同帧:`uPanoTiled` 是**按槽一个标记**(每点一个数,不是每张一个),
22
+ * 它告诉着色器「这几个槽装的是左右两张拼合槽(1) 还是一张整图(0)」。跨形态那一次(瓦片 ↔ 整片、
23
+ * 或某点压根没切过片)若逐张换,中间几帧会拿整图按半幅采(或反之)⇒ 瞬间拉伸错位。
24
+ * 所以「目标形态 ≠ 本槽现有形态」时走整档判齐口径。
25
+ * ⚠️ 同形态内也分两条路(09-25 治本):**整槽换人**(有格正贴着另一批的真图 ⇒ 全部同帧换,判不齐一张都不动)与
26
+ * **逐张补格**(本槽空着或同一批地址还缺的格 ⇒ 到一张补一张)。
27
+ * 「这一格认的是哪张地址」记在 `panoSlotTileKeys`,换人不成就连账本都不改,下一帧重试。
28
+ */
29
+ export function panoSlotBind(mat, panos, base, tiles, tiled, slot, deps) {
30
+ // 单全景材质的 uPanoTiled 是标量(写的时候忽略槽位);融合/映射材质是每点一个的数组 ⇒ 读法两样,判定同一个。
31
+ const uv = mat.uniforms.uPanoTiled?.value;
32
+ const curTiled = uv === undefined ? tiled
33
+ : (typeof uv === "number" ? uv > 0.5 : uv[slot ?? 0] > 0.5);
34
+ if (curTiled !== tiled) {
35
+ // ── 跨形态:整槽判齐、一次写入、同一帧翻标记(见上面 ⚠️)──
36
+ // ⭐ 09-28 旨「瓦片纹理不需要添加 oss 的参数」后,过渡缩图档与原图档下载的是**同一张图**,
37
+ // 「原图没齐先上缩图」的兜底只剩双下载 ⇒ 整条删除;判不齐就沿用上一批。
38
+ const ready = deps.peekSlot(tiles, tiled);
39
+ // ⛔ 判齐用「本点槽数」派生(不许写 `ready[0] && ready[1] && ready[2] && ready[3]`)
40
+ const all = ready.length >= RT_STITCH_SLOT_COUNT && ready.slice(0, RT_STITCH_SLOT_COUNT).every((t) => !!t);
41
+ if (all) {
42
+ const keys = panoSlotTileKeys(mat, panos.length);
43
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
44
+ panos[base + k] = ready[k];
45
+ keys[base + k] = tiles[k];
46
+ }
47
+ // ⚠️ 纹理与形态标记必须同一帧落地(分叉过一次 = 整片形态按瓦片 UV 采整图 ⇒ 半幅拉伸)
48
+ setMktPanoTiled(mat, tiled ? 1 : 0, slot);
49
+ return true;
50
+ }
51
+ // 整档还没齐:本槽沿用上一批(sampler 读到 undefined 会报错,所以压根没绑过才填占位)
52
+ if (!panos[base]) {
53
+ const keys = panoSlotTileKeys(mat, panos.length);
54
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
55
+ panos[base + k] = deps.placeholder;
56
+ keys[base + k] = tiles[k];
57
+ }
58
+ setMktPanoTiled(mat, tiled ? 1 : 0, slot); // 占位是 1×1 素色,两种采法无差别,直接把标记对准目标
59
+ }
60
+ return false;
61
+ }
62
+ // ── 同形态 ──
63
+ const keys = panoSlotTileKeys(mat, panos.length);
64
+ // ⭐ 换人(皇上旨 09-25「绝不让一张已显示的全景因为正在补第 3 格瓦片而整体退出」):
65
+ // 只要**有任何一格正贴着别的批次的真图**,本槽这次就是「整槽换人」⇒ 整槽所有张必须同帧落地,
66
+ // 判不齐就一张都不动(连账本也不动 ⇒ 下一帧照判「要换」继续重试,缺的那几张的下载/上传已由 `deps.peek` 当场发起)。
67
+ // 否则旧全景的第 1 张会配新全景的投影矩阵、且权重照给 ⇒ 墙上的图一会儿这张一会儿那张(正是他报的现象)。
68
+ let swapping = false;
69
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
70
+ const cur = panos[base + k];
71
+ if (cur && cur !== deps.placeholder && keys[base + k] !== tiles[k]) {
72
+ swapping = true;
73
+ break;
74
+ }
75
+ }
76
+ if (swapping) {
77
+ const got = [];
78
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
79
+ // ⭐ 09-28 旨后不再退缩图档(过渡档去参 = 同一张原图,兜底只剩双下载)
80
+ got.push(deps.peek(tiles[k], tiled));
81
+ }
82
+ if (!got.every((t) => !!t))
83
+ return false; // 沿用上一批:图 + 矩阵 + 账本一概留着
84
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
85
+ panos[base + k] = got[k];
86
+ keys[base + k] = tiles[k];
87
+ }
88
+ return true;
89
+ }
90
+ // ── 逐张补格(皇上的口径):走到这里说明本槽没有任何一格贴着别的批次的真图
91
+ // ⇒ 不是「首次那几帧」(整槽都是 1×1 占位),就是「同一批地址里还缺的格」。
92
+ // 两种都各判各的、到一张补一张,缺的格露的是素色占位而非另一张全景 ⇒ 无需攒齐。──
93
+ let bound = false;
94
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
95
+ const i = base + k;
96
+ // ⭐ 09-28 旨后不再退缩图档(过渡档去参 = 同一张原图,兜底只剩双下载)
97
+ const tex = deps.peek(tiles[k], tiled);
98
+ if (!tex)
99
+ continue; // 还在路上(缺的这张已由 deps.peek 当场发起下载 / 排上传)
100
+ keys[i] = tiles[k];
101
+ if (panos[i] !== tex)
102
+ panos[i] = tex;
103
+ bound = true;
104
+ }
105
+ return bound;
106
+ }
@@ -0,0 +1,153 @@
1
+ /*
2
+ * @Author: xiaosihan
3
+ * @Date: 2026-10-03
4
+ * @Description: 「一个点槽(同一个全景点的 `RT_STITCH_SLOT_COUNT` 张瓦片)如何换绑进 shader」的**唯一**策略实现
5
+ * (皇上旨 2026-10-03「整套搬进库(彻底同源)」)。
6
+ *
7
+ * ⭐ 2026-10-06 晚皇上旨「每个全景纹理由**左右 2 个 RT** 组成」⇒ 一个点槽的张数从 4 降到 **2**,
8
+ * 同屏全景数从 4 涨到 **8**。本文件里所有「几张」一律由 `RT_STITCH_SLOT_COUNT` 派生,
9
+ * ⛔ 不许再出现字面量 4(槽数再变会漏改 —— 这正是当年两侧分家的老毛病)。
10
+ *
11
+ * 来历:这套判据原先只活在编辑态 `mktMeshEditRenderer._bindSlotTiles`(皇上旨 09-24「一张一张替换上去,别 4 张一起换」
12
+ * + 09-25「未就绪整槽沿用上一批」+ 09-25「绝不让一张已显示的全景因为正在补第 3 格瓦片而整体退出」)。
13
+ * 库侧(预览 / 发布态 `MktMesh.feedFaceMapping`)走的是另一套「齐活才写、矩阵无条件 copy」⇒ ① 换景点会闪一帧灰,
14
+ * ② 图与投影矩阵可能来自**不同批**(旧图配新矩阵)。皇上看的是线上观感,两侧必须同一份代码,故整条搬进来。
15
+ *
16
+ * ⛔ 本文件是这个问题的**第一份也是唯一一份**实现:编辑器与库都只许「委托一行」并把各自的取图口注入进来。
17
+ * 出现第二份 = 两侧又会分家(护栏里有反钉)。
18
+ *
19
+ * 依赖注入的原因:编辑器与库的纹理来源不同(编辑器有自己的 `_floorTexCache` + `threeUtils` 上传队列;
20
+ * 库读自身那份),但「什么时候可以换、换要换几个、没换成该怎么办」这个**决策**必须完全一致 ⇒
21
+ * 决策留在本文件,取货与占位纹理由 `deps` 交进来。
22
+ */
23
+ import type { ShaderMaterial, Texture } from "three";
24
+ import { RT_STITCH_SLOT_COUNT, setMktPanoTiled } from "./mktShader";
25
+
26
+ /** 取一张「已解码**且**已上传 GPU」的瓦片;未就绪要**当场**自行起载 / 排上传,然后返回 null。
27
+ * ⚠️ 语义硬约束(皇上旨 09-25「队列里的代码就是把加载好的纹理设进 shader 这一句」):
28
+ * 本回调**不许**做同步的重活(解码、上传、绑定判断都不许在 render 里现场发生),只负责「发起 + 报告就绪」。 */
29
+ export interface PanoSlotBindDeps {
30
+ peek(key: string, original: boolean): Texture | null;
31
+ /** 整槽口径(**只**给跨形态翻转那一次用):同一个点的 `RT_STITCH_SLOT_COUNT` 张一起要,缺的返回 null。
32
+ * 编辑器实现 = `_peekSlotGen`(带 `proxy:` 过渡档墓碑分支),库实现 = 逐张 `peek`;
33
+ * 「判不齐就整档不换」这个**决定**仍然只在本文件里下。 */
34
+ peekSlot(tiles: string[], original: boolean): (Texture | null)[];
35
+ /** 该槽从没成功绑过任何图时用来垫 sampler 的 1×1 占位(⛔ 空 sampler 会报错) */
36
+ placeholder: Texture;
37
+ }
38
+
39
+ /**
40
+ * 「panoramas[i] 这一格认的是哪张瓦片地址」——逐张换绑的**账本**,按材质实例分 WeakMap 键。
41
+ * 放模块级 ⇒ 编辑器与库**共一本账**(两侧材质是不同实例,天然不串味);材质重建后自动从空账开始。
42
+ */
43
+ const slotTileKeys = new WeakMap<ShaderMaterial, string[]>();
44
+
45
+ /** 取(必要时建)某材质的逐格地址账本;`len` = `panoramas.length`(= 点数 × `RT_STITCH_SLOT_COUNT`)。 */
46
+ export function panoSlotTileKeys(mat: ShaderMaterial, len: number): string[] {
47
+ let keys = slotTileKeys.get(mat);
48
+ if (!keys) {
49
+ keys = new Array(len).fill("");
50
+ slotTileKeys.set(mat, keys);
51
+ }
52
+ return keys;
53
+ }
54
+
55
+ /**
56
+ * 把一个「点槽」的 `RT_STITCH_SLOT_COUNT` 张瓦片换绑进 `panoramas[base .. base + RT_STITCH_SLOT_COUNT - 1]`。
57
+ *
58
+ * 返回 true = 本槽这一帧的图与点位**配好了**(调用方据此写投影矩阵);false = 沿用上一批(调用方矩阵也别动)。
59
+ *
60
+ * ⚠️ 只有一条例外必须整槽同帧:`uPanoTiled` 是**按槽一个标记**(每点一个数,不是每张一个),
61
+ * 它告诉着色器「这几个槽装的是左右两张拼合槽(1) 还是一张整图(0)」。跨形态那一次(瓦片 ↔ 整片、
62
+ * 或某点压根没切过片)若逐张换,中间几帧会拿整图按半幅采(或反之)⇒ 瞬间拉伸错位。
63
+ * 所以「目标形态 ≠ 本槽现有形态」时走整档判齐口径。
64
+ * ⚠️ 同形态内也分两条路(09-25 治本):**整槽换人**(有格正贴着另一批的真图 ⇒ 全部同帧换,判不齐一张都不动)与
65
+ * **逐张补格**(本槽空着或同一批地址还缺的格 ⇒ 到一张补一张)。
66
+ * 「这一格认的是哪张地址」记在 `panoSlotTileKeys`,换人不成就连账本都不改,下一帧重试。
67
+ */
68
+ export function panoSlotBind(
69
+ mat: ShaderMaterial,
70
+ panos: Texture[],
71
+ base: number,
72
+ tiles: string[],
73
+ tiled: boolean,
74
+ slot: number | undefined,
75
+ deps: PanoSlotBindDeps,
76
+ ): boolean {
77
+ // 单全景材质的 uPanoTiled 是标量(写的时候忽略槽位);融合/映射材质是每点一个的数组 ⇒ 读法两样,判定同一个。
78
+ const uv = mat.uniforms.uPanoTiled?.value as number | ArrayLike<number> | undefined;
79
+ const curTiled = uv === undefined ? tiled
80
+ : (typeof uv === "number" ? uv > 0.5 : (uv[slot ?? 0] as number) > 0.5);
81
+
82
+ if (curTiled !== tiled) {
83
+ // ── 跨形态:整槽判齐、一次写入、同一帧翻标记(见上面 ⚠️)──
84
+ // ⭐ 09-28 旨「瓦片纹理不需要添加 oss 的参数」后,过渡缩图档与原图档下载的是**同一张图**,
85
+ // 「原图没齐先上缩图」的兜底只剩双下载 ⇒ 整条删除;判不齐就沿用上一批。
86
+ const ready = deps.peekSlot(tiles, tiled);
87
+ // ⛔ 判齐用「本点槽数」派生(不许写 `ready[0] && ready[1] && ready[2] && ready[3]`)
88
+ const all = ready.length >= RT_STITCH_SLOT_COUNT && ready.slice(0, RT_STITCH_SLOT_COUNT).every((t) => !!t);
89
+ if (all) {
90
+ const keys = panoSlotTileKeys(mat, panos.length);
91
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
92
+ panos[base + k] = ready[k] as Texture;
93
+ keys[base + k] = tiles[k];
94
+ }
95
+ // ⚠️ 纹理与形态标记必须同一帧落地(分叉过一次 = 整片形态按瓦片 UV 采整图 ⇒ 半幅拉伸)
96
+ setMktPanoTiled(mat, tiled ? 1 : 0, slot);
97
+ return true;
98
+ }
99
+ // 整档还没齐:本槽沿用上一批(sampler 读到 undefined 会报错,所以压根没绑过才填占位)
100
+ if (!panos[base]) {
101
+ const keys = panoSlotTileKeys(mat, panos.length);
102
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
103
+ panos[base + k] = deps.placeholder;
104
+ keys[base + k] = tiles[k];
105
+ }
106
+ setMktPanoTiled(mat, tiled ? 1 : 0, slot); // 占位是 1×1 素色,两种采法无差别,直接把标记对准目标
107
+ }
108
+ return false;
109
+ }
110
+
111
+ // ── 同形态 ──
112
+ const keys = panoSlotTileKeys(mat, panos.length);
113
+ // ⭐ 换人(皇上旨 09-25「绝不让一张已显示的全景因为正在补第 3 格瓦片而整体退出」):
114
+ // 只要**有任何一格正贴着别的批次的真图**,本槽这次就是「整槽换人」⇒ 整槽所有张必须同帧落地,
115
+ // 判不齐就一张都不动(连账本也不动 ⇒ 下一帧照判「要换」继续重试,缺的那几张的下载/上传已由 `deps.peek` 当场发起)。
116
+ // 否则旧全景的第 1 张会配新全景的投影矩阵、且权重照给 ⇒ 墙上的图一会儿这张一会儿那张(正是他报的现象)。
117
+ let swapping = false;
118
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
119
+ const cur = panos[base + k];
120
+ if (cur && cur !== deps.placeholder && keys[base + k] !== tiles[k]) {
121
+ swapping = true;
122
+ break;
123
+ }
124
+ }
125
+ if (swapping) {
126
+ const got: (Texture | null)[] = [];
127
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
128
+ // ⭐ 09-28 旨后不再退缩图档(过渡档去参 = 同一张原图,兜底只剩双下载)
129
+ got.push(deps.peek(tiles[k], tiled));
130
+ }
131
+ if (!got.every((t) => !!t)) return false; // 沿用上一批:图 + 矩阵 + 账本一概留着
132
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
133
+ panos[base + k] = got[k] as Texture;
134
+ keys[base + k] = tiles[k];
135
+ }
136
+ return true;
137
+ }
138
+
139
+ // ── 逐张补格(皇上的口径):走到这里说明本槽没有任何一格贴着别的批次的真图
140
+ // ⇒ 不是「首次那几帧」(整槽都是 1×1 占位),就是「同一批地址里还缺的格」。
141
+ // 两种都各判各的、到一张补一张,缺的格露的是素色占位而非另一张全景 ⇒ 无需攒齐。──
142
+ let bound = false;
143
+ for (let k = 0; k < RT_STITCH_SLOT_COUNT; k++) {
144
+ const i = base + k;
145
+ // ⭐ 09-28 旨后不再退缩图档(过渡档去参 = 同一张原图,兜底只剩双下载)
146
+ const tex = deps.peek(tiles[k], tiled);
147
+ if (!tex) continue; // 还在路上(缺的这张已由 deps.peek 当场发起下载 / 排上传)
148
+ keys[i] = tiles[k];
149
+ if (panos[i] !== tex) panos[i] = tex;
150
+ bound = true;
151
+ }
152
+ return bound;
153
+ }
@@ -0,0 +1,22 @@
1
+ /** ⭐ 槽位「中心死区」(米):离**锚点**(控制器中心点;发布页未注入时 = 相机位置)水平距离最近的那张是中心;挑战者必须比现任中心**近出这么多**才抢得走。
2
+ * 没有死区 ⇒ 锚点停在两点的等距带附近每帧换中心 ⇒ 换中心 = 整组邻居重排 = 一整组瓦片重绑(正是 09-24/25 报的「切全景卡一下」)。
3
+ * 定义在此是因为编辑态与库侧共用(全项目**只此一处**,两侧都 import 它)。 */
4
+ export declare const PANO_SLOT_SWAP_MARGIN_M = 0.5;
5
+ /** 一个候选:`t` 是全景点对象(存引用不存下标 —— 删点会让下标整体前移,引用不会认错人),`d` 是它到**锚点**的水平距离(锚点由调用方决定,本函数不认识它) */
6
+ export interface PanoRosterCand<T> {
7
+ t: T;
8
+ d: number;
9
+ }
10
+ /**
11
+ * 算出下一帧的槽位名册(**不修改传进来的 roster**,返回新数组)。
12
+ * @param roster 当前名册(0 号槽 = 上一帧的中心),长度可以小于 maxSlots(内部按 undefined 补齐)
13
+ * @param dists 本帧全部全景点及其到**锚点**的距离(锚点 = 调用方那把尺子的端点;⭐ 10-07 第二十一道旨后 = 控制器中心点),**数组顺序 = 全景列表顺序**(下标就是旨意里说的「几号全景」)
14
+ * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-06 晚旨后是 **8**:8 点 × 每个全景
15
+ * 左右 2 张 RT = 16 sampler 仍是 GLSL 物理上限。⛔ 本函数不许假设它恒等于 4)
16
+ * @param margin 抢中心的死区(米,= `PANO_SLOT_SWAP_MARGIN_M`)
17
+ * @returns 长度恒 = maxSlots 的新名册:**0 号槽 = 中心**,其余槽 = 列表里按序号一层层往外扩的那些
18
+ * (同一层先取离锚点更近那侧)。⭐ 10-07 旨「尽可能填满」⇒ 越界的序号不占格、外扩继续往有货那侧走,
19
+ * 所以**填几格 = min(项目全景总数, maxSlots)**;只有列表里的人不够时才留空(= `undefined`)。
20
+ * 中心没换人 ⇒ 在册的一格都不动,只把**空格**补上。
21
+ */
22
+ export declare function panoSlotRosterStep<T>(roster: (T | undefined)[], dists: PanoRosterCand<T>[], maxSlots: number, margin: number): (T | undefined)[];
@@ -0,0 +1,169 @@
1
+ /*
2
+ * ⭐ 槽位名册纯函数:**「中心 + 序号邻居」**定 shader 那 N 格装哪 N 张全景(皇上旨 2026-10-02 晚;
3
+ * 2026-10-06 晚旨「一次展示 8 个全景」⇒ N 从 4 变 8,靠「每个全景只占左右 2 张 RT」腾出 sampler)。
4
+ *
5
+ * 旨意原文(verbatim):「我想要把按距离筛选的逻辑, 改一改, 首先, 按照 全景到相机的距离选中第一个,
6
+ * 然后按照这个全景的序号, 前后各添加一个全景, 举个例子, 通过计算 5号 全景距离相机最近,
7
+ * 那就选出一个 5号全景填入 shader 然后再, 5号前后是 4号 和 6号, 也分别填入 shader,
8
+ * 最后再判断一下, 如果相机距离4号的距离 比 相机距离6号的距离近 就再追加一个[它外侧那张]全景, 反之就追加一个 7号 全景,
9
+ * 如果没有 目标全景, 比如 1号全景往前 没有 -1 -2 号全景, shader 里面就留空」
10
+ * ⭐ 同日追问三择(verbatim):「补 3 号(顺着往外扩一张)」/「中心没换人就维持整组」/
11
+ * 探针第一行「打成『中心#5 + 邻居 4/6/7』」。
12
+ * ⭐ 皇上旨 2026-10-07(第二十道,verbatim):「shader 是可以支持最多传入8个全景的, 所以尽可能得填满8个全景位置」
13
+ * ⇒ 收窄上面那句「如果没有目标全景…就留空」的适用边界:**留空只发生在「项目里的全景总数不够一格一人」时**;
14
+ * 序号越界的那个位置根本不是一张全景 ⇒ 跳过它、继续往还有货的那一侧扩,槽位不许白留给「不存在的序号」。
15
+ * 同一道旨还要求:新加的全景点**不必等中心换人**才进 shader ⇒ 中心没换人时在册的人照旧一格不动,空格当场补满。
16
+ * ⚠️ 上限仍是 `MKT_PANO_MAX_SLOTS`(=8):8 点 × 左右 2 张拼合槽 = 16 个 sampler,那是 GLSL 物理闸
17
+ * (皇上机器 maxTextures=16)⇒ 本函数不越那道闸,只是把那 8 格**尽量用满**。
18
+ * ⭐ 皇上旨 2026-10-07 第二十一道旨(verbatim):「判断使用那些全景球的判断条件, 从距离相机的距离 改成
19
+ * 判断距离控制器的中心点的距离来判断」⇒ **本函数的尺子另一端换了人**:函数体一个字不改(它只吃调用方算好的
20
+ * `d`),改的是**调用方**——编辑态与预览的锚点 = 轨道控制器注视点 `controls.target`(画面正中央那个点),
21
+ * 发布页那一路没有控制器 ⇒ 回落相机位置。下面所有「相机」二字请按「锚点(= 控制器中心点,未注入时才是相机)」读;
22
+ * 10-02 那道旨的原文里「距离相机最近」是**历史引文**,逐字保留。
23
+ * ⚠️ 同一把尺还喂着 `camW`(混合权重)⇒ 权重跟着换锚点;而面上「取哪一张」的 `score = 顶点距离 − 夹角`
24
+ * 里**根本没有相机量**(09-28 15:07 七修亲自撤掉 camD 项),不受本道旨影响。
25
+ *
26
+ * 一句话口径:
27
+ * 1) **中心** = 到**锚点**(调用方给的水平距离 XZ;锚点 = 控制器中心点,见上方 10-07 那道旨)最近那张;
28
+ * ⚠️ 带死区 `margin`:别人必须比在册中心**近出 margin 米以上**
29
+ * 才抢走中心 ⇒ 锚点在等距带抖动时整组一动不动(09-26 那条「其他 3 个依然不动」的稳定诉求搬到这里来用)。
30
+ * 2) 中心定了之后按**列表序号**(= 面板列表顺序 = 日志里那个 `#i`)取邻居:`中心-1`、`中心+1` 各一张。
31
+ * 3) 再往外一层层扩(`中心±2`、`中心±3`…)直到凑够槽数;**同一层里先取离锚点更近那一侧**
32
+ * (平手归序号小的那侧)。⭐ 2026-10-06 晚旨「一次展示 8 个」⇒ 槽数 4 → 8,这里从「只扩一层」
33
+ * 改成一层层扩下去;前 4 张的顺序与 4 槽时代**逐字相同**(中心、-1、+1、更近那侧的 ±2)。
34
+ * 4) ⭐ 2026-10-07 皇上旨「shader 是可以支持最多传入 8 个全景的, 所以尽可能得填满 8 个全景位置」
35
+ * ⇒ **改写 10-02 那句「没有目标全景就留空」的适用边界**:越界的那个**序号**直接跳过(它本来就不是一个全景),
36
+ * 外扩**继续往还有货的那一侧**走 ⇒ 只要列表里还有没用过的全景,槽就一直被填,直到凑够 `maxSlots`。
37
+ * ⛔ 只有「整个列表都被请进来了还不够一格一人的时候」(= 项目里的全景总数 < 槽数)才留空 ——
38
+ * 那是物理不够,不是策略留白。旧行为(中心贴列表两端 ⇒ 明明还有 5 张没用上却空着 3 格)作废。
39
+ * 5) ⭐ 中心没换人 ⇒ **在册的人一格都不动**(10-02 当日亲择,照旧);但 10-07 那道旨补了后半句:
40
+ * **空格要当场补上** ⇒ 新增的全景点不必等「中心换人」才进 shader(否则他一张一张加点时永远填不满)。
41
+ *
42
+ * ⚠️ 与历代口径的关系(别搞混):
43
+ * - 09-25「槽位与相机完全无关」→ 09-26「名册增量换人(一进一出)」→ 09-27「在册者按相机距离落槽、取最近 4 个」
44
+ * ⇒ 本函数把 09-27 那半条**作废**:不再取「最近 4 个」,改取「最近那一张的序号邻居」,槽号也不再按距离排。
45
+ * - 09-26 的「一进一出 + 0.5m 死区」精神保留,但对象从「谁挤掉谁」变成「谁能抢走中心」。
46
+ *
47
+ * ⚠️ 为什么 0 号槽必须是中心:立方体(`FurnitureObject`)那份 `BoxGeometry` **没有** `aPanoIndex` attribute ⇒
48
+ * 序号映射 shader 里 `vPanoIndex` 恒 0 ⇒ 立方体显示的就是名册第 0 格那一张(皇上旨 10-02「模型也按距离相机最近的方式
49
+ * 设置全景参数」的落点)。中心坐别的格 = 立方体显示错图。
50
+ *
51
+ * ⚠️ 为什么单独成文件:本函数零依赖(不碰 three / MobX / DOM),`test/mktmesh.test.js` 的 **AP 组**会用 esbuild
52
+ * 把**这份源码原文**转译后真跑一遍(中心/邻居/外扩/越界不占格/死区/删点/幂等/空格回填 各断言一次)。
53
+ * ⇒ 改这里的算法,那条数字测试立刻知道;⛔ 别把逻辑搬回渲染器里,那样就只能靠正则钉、证不了行为。
54
+ *
55
+ * ⭐ 皇上旨 2026-10-03「库侧也一并改成中心 ± 邻居」⇒ 本文件从 `src/views/MktMeshEdit/` **搬进库里**
56
+ * (`three-base/MktMesh/`):编辑态(`mktMeshEditRenderer`)与预览/发布态(`MktMesh.updateTexture` +
57
+ * `feedFaceMapping`)**共用这一份算法和下面那一个死区常数**,⛔ 不许在任一侧再抄一份 0.5 或另写一套挑图。
58
+ */
59
+ /** ⭐ 槽位「中心死区」(米):离**锚点**(控制器中心点;发布页未注入时 = 相机位置)水平距离最近的那张是中心;挑战者必须比现任中心**近出这么多**才抢得走。
60
+ * 没有死区 ⇒ 锚点停在两点的等距带附近每帧换中心 ⇒ 换中心 = 整组邻居重排 = 一整组瓦片重绑(正是 09-24/25 报的「切全景卡一下」)。
61
+ * 定义在此是因为编辑态与库侧共用(全项目**只此一处**,两侧都 import 它)。 */
62
+ export const PANO_SLOT_SWAP_MARGIN_M = 0.5;
63
+ /**
64
+ * 算出下一帧的槽位名册(**不修改传进来的 roster**,返回新数组)。
65
+ * @param roster 当前名册(0 号槽 = 上一帧的中心),长度可以小于 maxSlots(内部按 undefined 补齐)
66
+ * @param dists 本帧全部全景点及其到**锚点**的距离(锚点 = 调用方那把尺子的端点;⭐ 10-07 第二十一道旨后 = 控制器中心点),**数组顺序 = 全景列表顺序**(下标就是旨意里说的「几号全景」)
67
+ * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-06 晚旨后是 **8**:8 点 × 每个全景
68
+ * 左右 2 张 RT = 16 sampler 仍是 GLSL 物理上限。⛔ 本函数不许假设它恒等于 4)
69
+ * @param margin 抢中心的死区(米,= `PANO_SLOT_SWAP_MARGIN_M`)
70
+ * @returns 长度恒 = maxSlots 的新名册:**0 号槽 = 中心**,其余槽 = 列表里按序号一层层往外扩的那些
71
+ * (同一层先取离锚点更近那侧)。⭐ 10-07 旨「尽可能填满」⇒ 越界的序号不占格、外扩继续往有货那侧走,
72
+ * 所以**填几格 = min(项目全景总数, maxSlots)**;只有列表里的人不够时才留空(= `undefined`)。
73
+ * 中心没换人 ⇒ 在册的一格都不动,只把**空格**补上。
74
+ */
75
+ export function panoSlotRosterStep(roster, dists, maxSlots, margin) {
76
+ const next = roster.slice(0, maxSlots);
77
+ while (next.length < maxSlots)
78
+ next.push(undefined);
79
+ const alive = new Set(dists.map((n) => n.t));
80
+ // ① 已被删掉的点先腾槽(名册里躺着一个不存在的点 = 那格的矩阵与权重都是旧账)
81
+ for (let s = 0; s < maxSlots; s++)
82
+ if (next[s] && !alive.has(next[s]))
83
+ next[s] = undefined;
84
+ if (!dists.length)
85
+ return next.fill(undefined);
86
+ // ② 在册中心 = 0 号槽那位;别人必须比它近出 margin 以上才换中心
87
+ const incumbent = next[0] !== undefined && alive.has(next[0]) ? next[0] : undefined;
88
+ let nearest = 0;
89
+ for (let i = 1; i < dists.length; i++)
90
+ if (dists[i].d < dists[nearest].d)
91
+ nearest = i;
92
+ const incIdx = incumbent === undefined ? -1 : dists.findIndex((n) => n.t === incumbent);
93
+ const centerIdx = incIdx < 0 || dists[nearest].d < dists[incIdx].d - margin ? nearest : incIdx;
94
+ // ③ 目标清单 = 中心、列表前一张、后一张,然后**按「离锚点更近那一侧优先」一层层往外扩**,
95
+ // 直到凑够 maxSlots。⭐ 2026-10-06 晚皇上旨「一次能够展示 8 个全景纹理」⇒ maxSlots 由 4 变 8,
96
+ // 此处从「只扩一层」改成「一层层扩下去」。扩的**顺序**沿用当日那句「更近那侧再往外一张」:
97
+ // k=2 那一轮先取更近那侧 ⇒ 前 4 张与 4 槽时代**逐字相同**(中心、-1、+1、更近那侧的 ±2),
98
+ // 多出来的 4 张继续 ±3、±4…
99
+ // ⭐ 2026-10-07 旨「尽可能得填满 8 个全景位置」⇒ 越界的那个**序号**直接跳过、**不占一格**,
100
+ // 外扩继续往还有货的那一侧走(旧写法把 `undefined` 也塞进清单去占槽 ⇒ 10 个点、中心贴列表首端时
101
+ // 只填 5 格,剩下 5 张全景明明存在却进不了 shader)。清单里只装真实存在的全景
102
+ // ⇒ 填几格 = **min(项目全景总数, maxSlots)**,只有人不够时才留空(⛔ 不复制一张来凑数)。
103
+ const at = (i) => (i >= 0 && i < dists.length ? dists[i].t : undefined);
104
+ const lo = centerIdx - 1;
105
+ const hi = centerIdx + 1;
106
+ const dLo = lo >= 0 && lo < dists.length ? dists[lo].d : Infinity;
107
+ const dHi = hi >= 0 && hi < dists.length ? dists[hi].d : Infinity;
108
+ const nearSign = dLo <= dHi ? -1 : 1; // 更近那侧:-1 = 往前(序号更小),+1 = 往后;平手归序号小的那侧
109
+ const targets = [];
110
+ const push = (i) => {
111
+ if (targets.length >= maxSlots)
112
+ return;
113
+ const t = at(i);
114
+ if (t !== undefined)
115
+ targets.push(t);
116
+ };
117
+ push(centerIdx);
118
+ push(lo);
119
+ push(hi);
120
+ // k 超过列表长度 ⇒ 两侧都再也请不到人,循环自然停(⛔ 别写成「直到填满」的无界循环)
121
+ for (let k = 2; targets.length < maxSlots && k <= dists.length; k++) {
122
+ push(centerIdx + nearSign * k);
123
+ push(centerIdx - nearSign * k);
124
+ }
125
+ const used = new Set();
126
+ used.add(targets[0]);
127
+ // ④ ⭐ 中心没换人(10-02 当日亲择「维持整组」)⇒ 在册的人**一格都不动**,只把空格补上
128
+ // (10-07 那道旨的后半句):他一张一张加全景时,新增那张立刻进得了 shader,不用等中心换人;
129
+ // 删掉一张留下的那格也当场补。已有格子一字节都不重绑 ⇒ 没有任何换人/重传代价。
130
+ if (incIdx >= 0 && centerIdx === incIdx) {
131
+ const kept = next.slice();
132
+ kept[0] = targets[0];
133
+ for (const x of kept)
134
+ if (x !== undefined)
135
+ used.add(x);
136
+ for (const t of targets) {
137
+ if (used.has(t))
138
+ continue;
139
+ const hole = kept.indexOf(undefined);
140
+ if (hole < 1)
141
+ break; // -1 = 没有空格;0 = 0 号槽恒归中心(这里不可能发生,兜住)
142
+ kept[hole] = t;
143
+ used.add(t);
144
+ }
145
+ return kept;
146
+ }
147
+ // ⑤ 中心换人 ⇒ 整组按**新中心**的序号清单重算,但「仍在清单里的人留在原格」
148
+ // ⇒ 中心挪一格时只有必要的格重绑瓦片。0 号槽恒 = 中心(立方体 `vPanoIndex` 恒 0,它显示的就是这一格)。
149
+ const placed = new Array(maxSlots).fill(undefined);
150
+ placed[0] = targets[0];
151
+ const keepable = (x) => x !== undefined && targets.indexOf(x) >= 0 && !used.has(x);
152
+ for (let s = 1; s < maxSlots; s++) {
153
+ if (keepable(next[s])) {
154
+ placed[s] = next[s];
155
+ used.add(next[s]);
156
+ }
157
+ }
158
+ for (let s = 1; s < maxSlots; s++) {
159
+ if (placed[s] !== undefined)
160
+ continue;
161
+ const fresh = targets.find((x) => !used.has(x));
162
+ // 清单里的人全请完了还空 ⇒ 项目里的全景本来就不够一格一人(总数 < maxSlots),这一格留空
163
+ if (fresh === undefined)
164
+ continue;
165
+ placed[s] = fresh;
166
+ used.add(fresh);
167
+ }
168
+ return placed;
169
+ }