three-base 2.0.166 → 2.0.169

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.
@@ -76,8 +76,15 @@
76
76
  */
77
77
  import { ClampToEdgeWrapping, DataTexture, LinearFilter, LinearMipmapLinearFilter, Mesh, NoBlending, OrthographicCamera, PlaneGeometry, RGBAFormat, Scene, ShaderMaterial, SRGBColorSpace, UnsignedByteType, Vector3, Vector4, WebGLRenderTarget, } from "three";
78
78
  import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
79
+ // ⭐ 2026-10-09 皇上旨「上传也走 threeUtils.addWork 队列」:上传节流改为挂公共队列,见 `_uploadPass`。
80
+ import threeUtils from "../threeUtils";
79
81
  import { MISSING_TILE_GRAY, RT_STITCH_CELLS_PER_SLOT, RT_STITCH_SLOT_COUNT, RT_STITCH_SLOT_H, RT_STITCH_SLOT_TILES, RT_STITCH_SLOT_W, RT_STITCH_SLOT_GRID, RT_STITCH_TILES_PER_BATCH, rtStitchBatchOf, rtStitchCellPos, rtStitchTileInset, } from "./mktShader";
80
- /** 每帧最多几张**新**纹理进 GPU(旨②「每一帧同样也是只载入一张新纹理到 GPU 里面去」)*/
82
+ /**
83
+ * ⛔⛔ 2026-10-09 皇上旨:「每帧最多几张新纹理进 GPU」这条**节流已不再由此常量实现**,改由
84
+ * `threeUtils.addWork` 队列承担(见 `_uploadPass`)—— 因为函数内局部 budget 管不住「一帧多次 pump」。
85
+ * 此常量保留仅作**文档锚点**,标出「一帧一张」这个不可回退的设计承诺(改队列实现前先看这里)。
86
+ * ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
87
+ */
81
88
  const PANO_STITCH_UPLOADS_PER_FRAME = 1;
82
89
  /** 每帧最多发起几张下载(32 片一批,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次解码) */
83
90
  const PANO_STITCH_DOWNLOADS_PER_FRAME = 2;
@@ -141,6 +148,12 @@ export class PanoTileStitcher {
141
148
  _retryAt = new Map();
142
149
  _fails = new Map();
143
150
  _dead = new Set();
151
+ /** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
152
+ * ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
153
+ * 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
154
+ * —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
155
+ * 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
156
+ _uploadQueued = new Set();
144
157
  /** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
145
158
  _renderer = null;
146
159
  /** 推进计数(`progressStamp`)*/
@@ -198,10 +211,24 @@ export class PanoTileStitcher {
198
211
  }
199
212
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
200
213
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
201
- peek(batch, slot) {
214
+ peek(batch, slot, renderer) {
202
215
  const g = this._groups.get(batch);
203
- if (!g || !g.drawnCount[slot])
216
+ if (!g)
204
217
  return null;
218
+ if (!g.drawnCount[slot]) {
219
+ // ⭐ 皇上 2026-10-09 旨「切换全景时上一个全景的内容要渲染成深灰」:
220
+ // 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
221
+ // **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(皇上报的现象)。
222
+ // 这里改为:只要调用方给了 renderer,就**先把这张 RT 建出来**(`_ensureRt` 建完立刻
223
+ // 整张涂 `MISSING_TILE_GRAY` 缺失灰)并交出去 ⇒ 切换瞬间该象限就是**深灰底**,
224
+ // 随后随`pump` 逐格显影。观感 = 深灰 → 逐格填图,不再闪上一个全景的内容。
225
+ // ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
226
+ if (!renderer)
227
+ return null;
228
+ if (!this._renderer)
229
+ return null;
230
+ return this._ensureRt(this._renderer, g, slot).texture;
231
+ }
205
232
  const rt = g.rts[slot];
206
233
  return rt ? rt.texture : null;
207
234
  }
@@ -247,6 +274,7 @@ export class PanoTileStitcher {
247
274
  for (const batch of [...this._groups.keys()])
248
275
  this.disposeGroup(batch);
249
276
  this._texs.clear();
277
+ this._uploadQueued.clear(); // ⭐ 2026-10-09 旨:同上,连队列里那批未执行的项一并作废
250
278
  this._wanted.clear();
251
279
  this._retryAt.clear();
252
280
  this._fails.clear();
@@ -323,8 +351,28 @@ export class PanoTileStitcher {
323
351
  }
324
352
  /** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
325
353
  * 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
354
+ /**
355
+ * 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
356
+ *
357
+ * ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
358
+ * 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
359
+ * 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
360
+ * (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
361
+ * 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
362
+ *
363
+ * ⭐ 改挂`threeUtils.addWork` 的三个好处:
364
+ * ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
365
+ * ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
366
+ * ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
367
+ * 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
368
+ */
326
369
  _uploadPass(renderer) {
327
- let budget = PANO_STITCH_UPLOADS_PER_FRAME;
370
+ // ⛔⛔ 队列里已有待上传项 ⇒ **本次直接不排**。
371
+ // 这一句是本改法的关键闸门:若只靠下面 `has(rec)` 跳过「同一片」,遍历会**继续往下找下一片**
372
+ // 于是 pump 几次就把多张全排进队列(13 全景 × 32 片 = 416 项 ⇒ workList 膨胀、`includes` 退化成 O(n)、
373
+ // 队尾那张要等几百帧才传完)。先判「队列非空就收手」⇒ 队列恒 ≤2 项(一占位+一真活),延迟最低。
374
+ if (this._uploadQueued.size > 0)
375
+ return;
328
376
  for (const g of this._groups.values()) {
329
377
  if (g.pending <= 0)
330
378
  continue;
@@ -339,18 +387,25 @@ export class PanoTileStitcher {
339
387
  const rec = this._texs.get(url);
340
388
  if (!rec || !rec.tex || rec.uploaded)
341
389
  continue;
342
- this._uploadTile(rec);
343
- if (--budget <= 0)
344
- return; // 旨②「每一帧只载入一张新纹理」⇒ 这一趟到此为止
390
+ // ⛔ 已在队列里 ⇒ 跳过(`addWork` 按引用判重对「新闭包」无效 ⇒ 必须自备 Set,见 `_uploadQueued`)
391
+ if (this._uploadQueued.has(rec))
392
+ continue;
393
+ this._uploadQueued.add(rec);
394
+ threeUtils.addWork(() => { }); // 占一格(什么都不干),把真活推到下一个 tick
395
+ threeUtils.addWork(() => this._uploadTile(rec));
396
+ return; // ⛔ 本次 pump 只排一张:其余名额留给下一帧的队列,绝不在这一帧再排第二张
345
397
  }
346
398
  }
347
399
  }
348
400
  }
349
- /** 上传一片瓦片的纹理(`texImage2D` + mipmap 才是那一下贵的,所以单独成工序)。
350
- * ⚠️ 成败都记 `uploaded`:不记 ⇒ 那一格永远进不了 `_drawPass` ⇒ 该槽永远画不满、每帧白烧一个预算,
351
- * 比「那一帧没错峰」严重得多;记了最坏也只是退回 three 在绑定那一帧自己上传的老路径。
352
- * (与 `MktMesh._uploadTileTexture` 同一口径,皇上旨 2026-09-25「队列里只放上传那一句」。) */
401
+ /** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
402
+ * ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
403
+ * 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
404
+ * 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
405
+ * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
353
406
  _uploadTile(rec) {
407
+ if (!this._uploadQueued.delete(rec))
408
+ return;
354
409
  const renderer = this._renderer;
355
410
  if (!renderer || !rec.tex)
356
411
  return;
@@ -477,6 +532,8 @@ export class PanoTileStitcher {
477
532
  for (const batch of [...this._groups.keys()])
478
533
  this.disposeGroup(batch);
479
534
  this._texs.clear();
535
+ // ⭐ 2026-10-09 旨:在册组整份作废 ⇒ 上传队列里那些项的 rec 也一并作废(否则会拿旧 renderer 上传)
536
+ this._uploadQueued.clear();
480
537
  this._wanted.clear();
481
538
  this._retryAt.clear();
482
539
  this._fails.clear();
@@ -510,6 +567,9 @@ export class PanoTileStitcher {
510
567
  const rec = this._texs.get(url);
511
568
  if (rec && rec.tex)
512
569
  releasePanoTexture(rec.tex);
570
+ // ⭐ 2026-10-09 旨:已排进上传队列的那一片也要**作废**(否则队列回调会对已释放纹理 initTexture)
571
+ if (rec)
572
+ this._uploadQueued.delete(rec);
513
573
  this._texs.delete(url);
514
574
  this._wanted.delete(url);
515
575
  }
@@ -96,6 +96,8 @@ import {
96
96
  type WebGLRenderer,
97
97
  } from "three";
98
98
  import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
99
+ // ⭐ 2026-10-09 皇上旨「上传也走 threeUtils.addWork 队列」:上传节流改为挂公共队列,见 `_uploadPass`。
100
+ import threeUtils from "../threeUtils";
99
101
  import {
100
102
  MISSING_TILE_GRAY,
101
103
  RT_STITCH_CELLS_PER_SLOT,
@@ -110,7 +112,12 @@ import {
110
112
  rtStitchTileInset,
111
113
  } from "./mktShader";
112
114
 
113
- /** 每帧最多几张**新**纹理进 GPU(旨②「每一帧同样也是只载入一张新纹理到 GPU 里面去」)*/
115
+ /**
116
+ * ⛔⛔ 2026-10-09 皇上旨:「每帧最多几张新纹理进 GPU」这条**节流已不再由此常量实现**,改由
117
+ * `threeUtils.addWork` 队列承担(见 `_uploadPass`)—— 因为函数内局部 budget 管不住「一帧多次 pump」。
118
+ * 此常量保留仅作**文档锚点**,标出「一帧一张」这个不可回退的设计承诺(改队列实现前先看这里)。
119
+ * ⛔ 别再拿它去写 budget 循环:那正是「一帧上传 2 张」的根因(消费方 onBeforeRender 里 pump 了两次)。
120
+ */
114
121
  const PANO_STITCH_UPLOADS_PER_FRAME = 1;
115
122
 
116
123
  /** 每帧最多发起几张下载(32 片一批,⛔ 不加以限制就是一帧甩 32 条 XHR + 32 次解码) */
@@ -199,8 +206,14 @@ interface StitchGroup {
199
206
  export interface PanoStitcher {
200
207
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做) */
201
208
  ensure(urls: string[]): void;
202
- /** 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」);没画过 / 没这组返回 null。 */
203
- peek(batch: string, slot: number): Texture | null;
209
+ /**
210
+ * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
211
+ * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
212
+ * 先把 RT 建出来(建完即整张涂 `MISSING_TILE_GRAY` 缺失灰)并返回它** ⇒ 调用方切批次的那一帧
213
+ * 拿到的就是**深灰底**而非 null(null 会让它沿用上一批 ⇒ 墙上闪上一个全景)。
214
+ * ⛔ 没传 `renderer`(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
215
+ */
216
+ peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
204
217
  /** 每帧驱动(消费方每帧只调一次)。`renderer` = 调用方当前的 WebGLRenderer,还没捕获到就传 null
205
218
  * ⇒ 这一帧什么都不画(⛔ 不打假读数,也⛔ 不退回任何 CPU 拼合路径)。 */
206
219
  pump(renderer: WebGLRenderer | null): void;
@@ -230,6 +243,12 @@ export class PanoTileStitcher implements PanoStitcher {
230
243
  private _retryAt: Map<string, number> = new Map();
231
244
  private _fails: Map<string, number> = new Map();
232
245
  private _dead: Set<string> = new Set();
246
+ /** 已排进 `threeUtils.addWork` 队列、等着「只干上传那一句」的瓦片(皇上 2026-10-09 旨:上传走公共队列)。
247
+ * ⛔ 必须自备:`addWork` 用 `workList.includes(work)` 判重,而每次排的都是**新闭包** ⇒ 它判不出来。
248
+ * 这个 Set 兼作「作废哨兵」:`_uploadTile` 开头 `if (!this._uploadQueued.delete(rec)) return;`
249
+ * —— 该瓦片若已先被 `_dropTile`/`_switchRenderer`清掉(dispose 过),这一项就作废,
250
+ * 否则会对已释放的纹理 `initTexture` ⇒ 凭空建显存且无人回收。 */
251
+ private _uploadQueued: Set<TileRec> = new Set();
233
252
  /** 建 RT 时用的渲染器(换 renderer ⇒ 在册组全清,见文件头) */
234
253
  private _renderer: WebGLRenderer | null = null;
235
254
  /** 推进计数(`progressStamp`)*/
@@ -288,9 +307,21 @@ export class PanoTileStitcher implements PanoStitcher {
288
307
 
289
308
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
290
309
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
291
- peek(batch: string, slot: number): Texture | null {
310
+ peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null {
292
311
  const g = this._groups.get(batch);
293
- if (!g || !g.drawnCount[slot]) return null;
312
+ if (!g) return null;
313
+ if (!g.drawnCount[slot]) {
314
+ // ⭐ 皇上 2026-10-09 旨「切换全景时上一个全景的内容要渲染成深灰」:
315
+ // 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
316
+ // **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(皇上报的现象)。
317
+ // 这里改为:只要调用方给了 renderer,就**先把这张 RT 建出来**(`_ensureRt` 建完立刻
318
+ // 整张涂 `MISSING_TILE_GRAY` 缺失灰)并交出去 ⇒ 切换瞬间该象限就是**深灰底**,
319
+ // 随后随`pump` 逐格显影。观感 = 深灰 → 逐格填图,不再闪上一个全景的内容。
320
+ // ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null —— 不能在无 GL 上下文时凭空建 RT。
321
+ if (!renderer) return null;
322
+ if (!this._renderer) return null;
323
+ return this._ensureRt(this._renderer, g, slot).texture;
324
+ }
294
325
  const rt = g.rts[slot];
295
326
  return rt ? rt.texture : null;
296
327
  }
@@ -338,6 +369,7 @@ export class PanoTileStitcher implements PanoStitcher {
338
369
  dispose(): void {
339
370
  for (const batch of [...this._groups.keys()]) this.disposeGroup(batch);
340
371
  this._texs.clear();
372
+ this._uploadQueued.clear(); // ⭐ 2026-10-09 旨:同上,连队列里那批未执行的项一并作废
341
373
  this._wanted.clear();
342
374
  this._retryAt.clear();
343
375
  this._fails.clear();
@@ -408,8 +440,27 @@ export class PanoTileStitcher implements PanoStitcher {
408
440
 
409
441
  /** 工序 3:每帧至多把**一张**新纹理传进 GPU(旨②),这一趟只上传、不画。
410
442
  * 按「槽 0 格 0 → …」的顺序挑第一张已解码没上传的 ⇒ RT 从左上角起逐格显影,观感可预期。 */
443
+ /**
444
+ * 工序 3:**只入队一张**,真正上传交给 `threeUtils.addWork`(皇上 2026-10-09 旨)。
445
+ *
446
+ * ⚠️⛔ 为什么不用「函数内局部 budget」了:那个 `budget` 每次调用都重新初始化为 1,
447
+ * 只管得住「**本次 pump 内**≤1 张」,管不住「一帧被 pump 多次」——
448
+ * 而消费方 `MktMesh.onBeforeRender` 里 `updateTexture` 与 `feedFaceMapping` **各调了一次 pump**
449
+ * (两处注释都以为对方在驱动,实际各驱动一次)⇒ 实际每帧上传 2 张,
450
+ * 违背本文件头「每一帧同样也是只载入一张新纹理」的承诺(实测 Performance 面板一帧看到多次 initTexture)。
451
+ *
452
+ * ⭐ 改挂`threeUtils.addWork` 的三个好处:
453
+ * ① 节流**跨调用天然成立** —— 队列每帧只`splice(0,1)` 取一项,无论 pump 调几次都只上传一张;
454
+ * ② 与库内既有范式同口径(`MktMesh._commitTileToShader` 就是这么排的),上传路径全走同一条队列;
455
+ * ③ 保留皇上 10-05 旨的「占一格」手法(下面 `addWork(() => {})`)⇒ 真活晚一帧执行,
456
+ * 与 `_drawPass` 天然错开(上传与绘制不落同一拍,正是 09-29「两个大头别在同一帧」的延续)。
457
+ */
411
458
  private _uploadPass(renderer: WebGLRenderer): void {
412
- let budget = PANO_STITCH_UPLOADS_PER_FRAME;
459
+ // ⛔⛔ 队列里已有待上传项 ⇒ **本次直接不排**。
460
+ // 这一句是本改法的关键闸门:若只靠下面 `has(rec)` 跳过「同一片」,遍历会**继续往下找下一片**
461
+ // 于是 pump 几次就把多张全排进队列(13 全景 × 32 片 = 416 项 ⇒ workList 膨胀、`includes` 退化成 O(n)、
462
+ // 队尾那张要等几百帧才传完)。先判「队列非空就收手」⇒ 队列恒 ≤2 项(一占位+一真活),延迟最低。
463
+ if (this._uploadQueued.size > 0) return;
413
464
  for (const g of this._groups.values()) {
414
465
  if (g.pending <= 0) continue;
415
466
  for (let slot = 0; slot < RT_STITCH_SLOT_COUNT; slot++) {
@@ -420,18 +471,24 @@ export class PanoTileStitcher implements PanoStitcher {
420
471
  if (!url || this._dead.has(url)) continue;
421
472
  const rec = this._texs.get(url);
422
473
  if (!rec || !rec.tex || rec.uploaded) continue;
423
- this._uploadTile(rec);
424
- if (--budget <= 0) return; // 旨②「每一帧只载入一张新纹理」⇒ 这一趟到此为止
474
+ // ⛔ 已在队列里 ⇒ 跳过(`addWork` 按引用判重对「新闭包」无效 ⇒ 必须自备 Set,见 `_uploadQueued`)
475
+ if (this._uploadQueued.has(rec)) continue;
476
+ this._uploadQueued.add(rec);
477
+ threeUtils.addWork(() => {}); // 占一格(什么都不干),把真活推到下一个 tick
478
+ threeUtils.addWork(() => this._uploadTile(rec));
479
+ return; // ⛔ 本次 pump 只排一张:其余名额留给下一帧的队列,绝不在这一帧再排第二张
425
480
  }
426
481
  }
427
482
  }
428
483
  }
429
484
 
430
- /** 上传一片瓦片的纹理(`texImage2D` + mipmap 才是那一下贵的,所以单独成工序)。
431
- * ⚠️ 成败都记 `uploaded`:不记 ⇒ 那一格永远进不了 `_drawPass` ⇒ 该槽永远画不满、每帧白烧一个预算,
432
- * 比「那一帧没错峰」严重得多;记了最坏也只是退回 three 在绑定那一帧自己上传的老路径。
433
- * (与 `MktMesh._uploadTileTexture` 同一口径,皇上旨 2026-09-25「队列里只放上传那一句」。) */
485
+ /** 队列回调的全部内容:把这**一片**瓦片的纹理上传到 GPU(`texImage2D` + mipmap 才是那一下贵的)。
486
+ * ⚠️ 开头那句判空是**作废闸**(皇上 2026-10-09 旨加):本项在队列里等tick 期间,
487
+ * 那片瓦片可能已被 `_dropTile`(画完即弃)/ `_switchRenderer` 清掉并 dispose ⇒ 此时必须作废,
488
+ * 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
489
+ * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
434
490
  private _uploadTile(rec: TileRec): void {
491
+ if (!this._uploadQueued.delete(rec)) return;
435
492
  const renderer = this._renderer;
436
493
  if (!renderer || !rec.tex) return;
437
494
  try {
@@ -562,6 +619,8 @@ export class PanoTileStitcher implements PanoStitcher {
562
619
  this._renderer = renderer;
563
620
  for (const batch of [...this._groups.keys()]) this.disposeGroup(batch);
564
621
  this._texs.clear();
622
+ // ⭐ 2026-10-09 旨:在册组整份作废 ⇒ 上传队列里那些项的 rec 也一并作废(否则会拿旧 renderer 上传)
623
+ this._uploadQueued.clear();
565
624
  this._wanted.clear();
566
625
  this._retryAt.clear();
567
626
  this._fails.clear();
@@ -595,6 +654,8 @@ export class PanoTileStitcher implements PanoStitcher {
595
654
  private _dropTile(url: string): void {
596
655
  const rec = this._texs.get(url);
597
656
  if (rec && rec.tex) releasePanoTexture(rec.tex);
657
+ // ⭐ 2026-10-09 旨:已排进上传队列的那一片也要**作废**(否则队列回调会对已释放纹理 initTexture)
658
+ if (rec) this._uploadQueued.delete(rec);
598
659
  this._texs.delete(url);
599
660
  this._wanted.delete(url);
600
661
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-base",
3
- "version": "2.0.166",
3
+ "version": "2.0.169",
4
4
  "description": "A Three.js extension library providing common 3D components, animations, materials, and utilities",
5
5
  "author": "xiaosihan",
6
6
  "publishConfig": {