three-base 2.0.193 → 2.0.195

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.
@@ -8,10 +8,21 @@ export interface PanoStitcher {
8
8
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
9
9
  * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
10
10
  * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。
11
- * 新增可选 `front` 参数(2026-10-29 需求):`true` ⇒ 这一批**挪到在册顺序最前** = 每帧那三道工序
12
- * 先取它的料(下载额度「每帧 2 张」先归它)。消费方每帧点一次 ⇒ 换渲染器那次整份作废后,
13
- * 下一帧又把头名抢回来(⛔ 光靠「先入册」做不到,见 `MktMesh.prefetchHeadPanoramas` 的说明)。 */
14
- ensure(urls: string[], layout?: TileLayout, front?: boolean): void;
11
+ * ⚠️ 2026-10-30「加载任务队列」:本方法只负责**入册**(新组追加到在册顺序末尾 ⇒ 排在队列里等),
12
+ * 点名优先的那一路换成交换 `prioritize()`(10-29 那版挂在第三个参 `front` 上的「挪到队首」已并进那里,
13
+ * ⛔ 一处排队只有 `prioritize` 一张尺)。 */
14
+ ensure(urls: string[], layout?: TileLayout): void;
15
+ /**
16
+ * 把某批瓦片登记进**加载任务队列**(2026-10-30 需求,逐字:「依次把前 2 个全景的顺序把瓦片加载任务添加到
17
+ * 任务队列里面,第一个加载完了,就把这个加载任务从任务队列里面去掉,然后加载第二个;后续按距离需要哪个全景
18
+ * 的时候同样添加到队列后面等待加载」)。
19
+ * 语义 = 先 `ensure` 入册(不在册就建组),再把批次键登记进队列**末尾**(已在队列里就不再挪,所以先登记的排前面)。
20
+ * 排队规则见 `_pumpDownloads`:**队首那组落定(32 格全画进 RT,或该格本就缺地址/判死)之前,队列里后面的组
21
+ * 和一切没登记的名册组一片都不发** ⇒ 第 1 颗全景独享全部下载额度、最快出全图。
22
+ * ⚠️ 消费方每帧点一次(`MktMesh.prefetchHeadPanoramas`):预览每开一次都换渲染器,`_switchRenderer` 会把在册组
23
+ * 整份作废 ⇒ 队列跟着清空,下一帧重新登记才能把顺序抢回来(只在 `importData` 里点一次等于白点)。
24
+ */
25
+ prioritize(urls: string[], layout?: TileLayout): void;
15
26
  /**
16
27
  * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
17
28
  * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
@@ -40,6 +51,10 @@ export interface PanoStitcher {
40
51
  export declare class PanoTileStitcher implements PanoStitcher {
41
52
  /** 批次组(键 = 32 张瓦片的第一张**非空**地址,与消费方同一个 `rtStitchBatchOf`) */
42
53
  private _groups;
54
+ /** 加载任务队列(2026-10-30 需求):批次键的**串行带**,队首那组落定才开下一组。
55
+ * 由 `prioritize` 登记(存档里前 2 颗全景),`_pumpDownloads` 每帧出清;`_switchRenderer`/`dispose`/`disposeGroup`
56
+ * 跟着组一起拆。⛔ 这是「谁先下载」的唯一一把尺 —— 在册顺序(`_groups` 的 Map 序)只决定「同不带队列时」的补料次序。 */
57
+ private _queue;
43
58
  /** 瓦片纹理(url → `{tex, uploaded}`)。画进自己那一格就立刻释放 ⇒ 只在册期间占显存/内存。 */
44
59
  private _texs;
45
60
  /** 仍在册的组还需要的地址(迟到的加载回调据此判断「这条已经没人要了,立刻释放」) */
@@ -77,10 +92,14 @@ export declare class PanoTileStitcher implements PanoStitcher {
77
92
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
78
93
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
79
94
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
80
- ensure(urls: string[], layout?: TileLayout, front?: boolean): void;
81
- /** 把一批挪到在册顺序的**最前**(只动 Map 的顺序,⛔ 不动组里任何东西:RT、已画格子、瓦片账本原样)。
82
- * 已在最前 ⇒ 一次 `keys().next()` 就返回(每帧点名走的就是这条零成本支路)。 */
83
- private _toFront;
95
+ ensure(urls: string[], layout?: TileLayout): void;
96
+ /** 登记进加载任务队列(语义与排队规则见接口上那一段)。已在队列里 ⇒ 不挪(先登记的仍在前)。 */
97
+ prioritize(urls: string[], layout?: TileLayout): void;
98
+ /** 地址数组 → 批次键(补齐到满批再取首条非空,与 `ensure` 里同一条尺 ⇒ 两处不会各算各的)。
99
+ * 超限或全空 ⇒ 空串(`ensure` 那一步同样不收,这里也不入队)。 */
100
+ private _batchKeyOf;
101
+ /** 这一组的每一格要么已经画进 RT,要么根本没有地址 / 地址已判死 ⇒ 该下的都下完了(队列出清判据)。 */
102
+ private _isSettled;
84
103
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
85
104
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
86
105
  peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
@@ -92,14 +111,20 @@ export declare class PanoTileStitcher implements PanoStitcher {
92
111
  progressStamp(): number;
93
112
  /** 每帧驱动(三道工序,顺序固定:补下载 → 画已进 GPU 的 → 再传一张新的)。 */
94
113
  pump(renderer: WebGLRenderer | null): void;
95
- /** 释放整组(2 张 RT + 该组还没画掉的瓦片纹理) */
114
+ /** 释放一整组(2 张 RT + 该组还没画掉的瓦片纹理);队列里那一记任务跟着撤(组没了还排着 = 队首永远等不到落定) */
96
115
  disposeGroup(batch: string): void;
97
116
  /** 释放全部(持有者 `dispose()` 时调用);共享的那套 quad/材质/空白纹理一并退掉 */
98
117
  dispose(): void;
99
118
  private _now;
100
- /** 工序 1:按「槽 0 格 0 → 槽 3 格 15」的顺序补齐还没发起的下载,受三道闸约束(每帧 2 张 /
101
- * 下载中 ≤8 / 已解码待上传 ≤8)⇒ 一片瓦片终身只 `fetch` 一次(在册条目不重复发)。 */
119
+ /** 工序 1:按「槽 0 格 0 → …」的顺序补齐还没发起的下载,受三道闸约束(每帧 2 张 / 下载中 ≤8 /
120
+ * 已解码待上传 ≤8)⇒ 一片瓦片终身只 `fetch` 一次(在册条目不重复发)。
121
+ * ⭐ 2026-10-30「加载任务队列」:队列非空时**只喂队首那一组**,它落定(`_isSettled`)就出队、下一帧轮到下一个;
122
+ * ⇒ 存档里第 1 颗全景独享额度下满,才轮到第 2 颗,之后才轮到名册按距离点到、排在队尾等着的那些全景。
123
+ * 队列为空 ⇒ 退回原来的在册顺序逐组补料(图片平面那一路不入队,照旧按在册顺序)。 */
102
124
  private _pumpDownloads;
125
+ /** 某一组的取料循环:把「还没画、有地址、没判死、没发起过」的格子交给 `_loadTile`。
126
+ * 返回 false = 三道闸里有一道合上了 ⇒ 本帧别再往下看(`ctx` 是连扣的,跨组也接着算)。 */
127
+ private _issueCells;
103
128
  /** 工序 2:把所有「已进 GPU 且还没画」的格子画进各自的 RT —— 一帧画多少格都行(旨②)。
104
129
  * ⛔ 准入只看一件事:`uploaded`。没上传过的纹理当场画 = `texImage2D` 藏进 `render()` 里
105
130
  * = 每帧一张新纹理那道闸形同虚设(这正是本轮要治的卡顿本体)。 */
@@ -41,6 +41,8 @@
41
41
  *
42
42
  * ── 每帧的三道工序(旨②那两句话逐条落地,顺序固定)────────────────────────────────
43
43
  * 1. `_pumpDownloads()`:补发缺图的下载(每帧 ≤2 张、在册 ≤8、已解码待上传 ≤8)⇒ ⛔ 一批几十条 XHR 一起炸。
44
+ * ⭐ 2026-10-30 需求「加载任务队列」:`_queue` 非空时**只喂队首那一组**,那一组落定才出队开下一组
45
+ * ⇒ 存档前 2 颗全景是一颗一颗下的(第 1 颗下满才轮到第 2 颗),名册按距离点到的排在后面等。
44
46
  * 2. `_drawPass()`:**只画「已经在 GPU 上的」瓦片**,一帧内画多少格都行(旨②「已经载入过的纹理,才可以
45
47
  * 一次性全部渲染到 RT 上面去」)。画完立刻释放那片 ⇒ 显存峰值 = 一格的量。
46
48
  * 3. `_uploadPass()`:**每帧至多一张**新纹理 `initTexture` 进 GPU(旨②「每一帧同样也是只载入一张新纹理
@@ -166,6 +168,10 @@ void main() {
166
168
  export class PanoTileStitcher {
167
169
  /** 批次组(键 = 32 张瓦片的第一张**非空**地址,与消费方同一个 `rtStitchBatchOf`) */
168
170
  _groups = new Map();
171
+ /** 加载任务队列(2026-10-30 需求):批次键的**串行带**,队首那组落定才开下一组。
172
+ * 由 `prioritize` 登记(存档里前 2 颗全景),`_pumpDownloads` 每帧出清;`_switchRenderer`/`dispose`/`disposeGroup`
173
+ * 跟着组一起拆。⛔ 这是「谁先下载」的唯一一把尺 —— 在册顺序(`_groups` 的 Map 序)只决定「同不带队列时」的补料次序。 */
174
+ _queue = [];
169
175
  /** 瓦片纹理(url → `{tex, uploaded}`)。画进自己那一格就立刻释放 ⇒ 只在册期间占显存/内存。 */
170
176
  _texs = new Map();
171
177
  /** 仍在册的组还需要的地址(迟到的加载回调据此判断「这条已经没人要了,立刻释放」) */
@@ -220,20 +226,17 @@ export class PanoTileStitcher {
220
226
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
221
227
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
222
228
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
223
- ensure(urls, layout, front = false) {
229
+ ensure(urls, layout) {
224
230
  const L = layout ?? PANO_TILE_LAYOUT;
225
231
  const maxTiles = tilesPerBatchOf(L);
226
232
  if (!urls || urls.length === 0 || urls.length > maxTiles)
227
233
  return;
234
+ const batch = this._batchKeyOf(urls, L);
235
+ if (this._groups.has(batch))
236
+ return;
228
237
  const u = urls.slice();
229
238
  while (u.length < maxTiles)
230
239
  u.push("");
231
- const batch = rtStitchBatchOf(u);
232
- if (this._groups.has(batch)) {
233
- if (front)
234
- this._toFront(batch);
235
- return;
236
- }
237
240
  const slotCount = L.slotCount;
238
241
  const cellsPerSlot = cellsPerSlotOf(L);
239
242
  this._groups.set(batch, {
@@ -247,25 +250,41 @@ export class PanoTileStitcher {
247
250
  drawnCount: Array.from({ length: slotCount }, () => 0),
248
251
  pending: slotCount,
249
252
  });
250
- if (front)
251
- this._toFront(batch);
252
253
  this._stamp++;
253
254
  }
254
- /** 把一批挪到在册顺序的**最前**(只动 Map 的顺序,⛔ 不动组里任何东西:RT、已画格子、瓦片账本原样)。
255
- * 已在最前 ⇒ 一次 `keys().next()` 就返回(每帧点名走的就是这条零成本支路)。 */
256
- _toFront(batch) {
257
- const g = this._groups.get(batch);
258
- if (!g)
255
+ /** 登记进加载任务队列(语义与排队规则见接口上那一段)。已在队列里 ⇒ 不挪(先登记的仍在前)。 */
256
+ prioritize(urls, layout) {
257
+ this.ensure(urls, layout);
258
+ const batch = this._batchKeyOf(urls, layout ?? PANO_TILE_LAYOUT);
259
+ if (!batch || !this._groups.has(batch) || this._queue.includes(batch))
259
260
  return;
260
- const head = this._groups.keys().next();
261
- if (!head.done && head.value === batch)
262
- return;
263
- const rest = [...this._groups.entries()];
264
- this._groups.clear();
265
- this._groups.set(batch, g);
266
- for (const [k, v] of rest)
267
- if (k !== batch)
268
- this._groups.set(k, v);
261
+ this._queue.push(batch);
262
+ }
263
+ /** 地址数组 → 批次键(补齐到满批再取首条非空,与 `ensure` 里同一条尺 ⇒ 两处不会各算各的)。
264
+ * 超限或全空 ⇒ 空串(`ensure` 那一步同样不收,这里也不入队)。 */
265
+ _batchKeyOf(urls, L) {
266
+ const maxTiles = tilesPerBatchOf(L);
267
+ if (!urls || urls.length === 0 || urls.length > maxTiles)
268
+ return "";
269
+ const u = urls.slice(0, maxTiles);
270
+ while (u.length < maxTiles)
271
+ u.push("");
272
+ return rtStitchBatchOf(u);
273
+ }
274
+ /** 这一组的每一格要么已经画进 RT,要么根本没有地址 / 地址已判死 ⇒ 该下的都下完了(队列出清判据)。 */
275
+ _isSettled(g) {
276
+ for (let slot = 0; slot < g.layout.slotCount; slot++) {
277
+ const ids = g.slotTiles[slot];
278
+ for (let cell = 0; cell < g.cellsPerSlot; cell++) {
279
+ if (g.drawn[slot] & (1 << cell))
280
+ continue;
281
+ const url = g.urls[ids[cell]];
282
+ if (!url || this._dead.has(url))
283
+ continue;
284
+ return false;
285
+ }
286
+ }
287
+ return true;
269
288
  }
270
289
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
271
290
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
@@ -318,13 +337,16 @@ export class PanoTileStitcher {
318
337
  this._drawPass(renderer);
319
338
  this._uploadPass(renderer);
320
339
  }
321
- /** 释放整组(2 张 RT + 该组还没画掉的瓦片纹理) */
340
+ /** 释放一整组(2 张 RT + 该组还没画掉的瓦片纹理);队列里那一记任务跟着撤(组没了还排着 = 队首永远等不到落定) */
322
341
  disposeGroup(batch) {
323
342
  const g = this._groups.get(batch);
324
343
  if (!g)
325
344
  return;
326
345
  this._dropGroupGpu(g);
327
346
  this._groups.delete(batch);
347
+ const qi = this._queue.indexOf(batch);
348
+ if (qi >= 0)
349
+ this._queue.splice(qi, 1);
328
350
  this._stamp++;
329
351
  }
330
352
  /** 释放全部(持有者 `dispose()` 时调用);共享的那套 quad/材质/空白纹理一并退掉 */
@@ -346,38 +368,58 @@ export class PanoTileStitcher {
346
368
  _now() {
347
369
  return typeof performance !== "undefined" ? performance.now() : Date.now();
348
370
  }
349
- /** 工序 1:按「槽 0 格 0 → 槽 3 格 15」的顺序补齐还没发起的下载,受三道闸约束(每帧 2 张 /
350
- * 下载中 ≤8 / 已解码待上传 ≤8)⇒ 一片瓦片终身只 `fetch` 一次(在册条目不重复发)。 */
371
+ /** 工序 1:按「槽 0 格 0 → …」的顺序补齐还没发起的下载,受三道闸约束(每帧 2 张 / 下载中 ≤8 /
372
+ * 已解码待上传 ≤8)⇒ 一片瓦片终身只 `fetch` 一次(在册条目不重复发)。
373
+ * ⭐ 2026-10-30「加载任务队列」:队列非空时**只喂队首那一组**,它落定(`_isSettled`)就出队、下一帧轮到下一个;
374
+ * ⇒ 存档里第 1 颗全景独享额度下满,才轮到第 2 颗,之后才轮到名册按距离点到、排在队尾等着的那些全景。
375
+ * 队列为空 ⇒ 退回原来的在册顺序逐组补料(图片平面那一路不入队,照旧按在册顺序)。 */
351
376
  _pumpDownloads() {
352
- let inflight = 0;
353
- let decoded = 0;
377
+ while (this._queue.length > 0) {
378
+ const head = this._groups.get(this._queue[0]);
379
+ if (head && !this._isSettled(head))
380
+ break;
381
+ this._queue.shift();
382
+ }
383
+ const ctx = { budget: PANO_STITCH_DOWNLOADS_PER_FRAME, inflight: 0, decoded: 0 };
354
384
  for (const rec of this._texs.values()) {
355
385
  if (rec.tex)
356
- decoded++;
386
+ ctx.decoded++;
357
387
  else
358
- inflight++;
388
+ ctx.inflight++;
389
+ }
390
+ if (this._queue.length > 0) {
391
+ const head = this._groups.get(this._queue[0]);
392
+ if (head)
393
+ this._issueCells(head, ctx);
394
+ return;
359
395
  }
360
- let budget = PANO_STITCH_DOWNLOADS_PER_FRAME;
361
396
  for (const g of this._groups.values()) {
362
- if (budget <= 0 || inflight >= PANO_STITCH_MAX_INFLIGHT || decoded >= PANO_STITCH_MAX_DECODED_AHEAD)
397
+ if (ctx.budget <= 0 || ctx.inflight >= PANO_STITCH_MAX_INFLIGHT || ctx.decoded >= PANO_STITCH_MAX_DECODED_AHEAD)
363
398
  return;
364
- for (let slot = 0; slot < g.layout.slotCount; slot++) {
365
- const ids = g.slotTiles[slot];
366
- for (let cell = 0; cell < g.cellsPerSlot; cell++) {
367
- if (g.drawn[slot] & (1 << cell))
368
- continue;
369
- const url = g.urls[ids[cell]];
370
- if (!url || this._dead.has(url))
371
- continue;
372
- if (this._texs.has(url))
373
- continue;
374
- this._loadTile(url);
375
- inflight++;
376
- if (--budget <= 0 || inflight >= PANO_STITCH_MAX_INFLIGHT || decoded >= PANO_STITCH_MAX_DECODED_AHEAD)
377
- return;
378
- }
399
+ if (!this._issueCells(g, ctx))
400
+ return;
401
+ }
402
+ }
403
+ /** 某一组的取料循环:把「还没画、有地址、没判死、没发起过」的格子交给 `_loadTile`。
404
+ * 返回 false = 三道闸里有一道合上了 ⇒ 本帧别再往下看(`ctx` 是连扣的,跨组也接着算)。 */
405
+ _issueCells(g, ctx) {
406
+ for (let slot = 0; slot < g.layout.slotCount; slot++) {
407
+ const ids = g.slotTiles[slot];
408
+ for (let cell = 0; cell < g.cellsPerSlot; cell++) {
409
+ if (g.drawn[slot] & (1 << cell))
410
+ continue;
411
+ const url = g.urls[ids[cell]];
412
+ if (!url || this._dead.has(url))
413
+ continue;
414
+ if (this._texs.has(url))
415
+ continue;
416
+ this._loadTile(url);
417
+ ctx.inflight++;
418
+ if (--ctx.budget <= 0 || ctx.inflight >= PANO_STITCH_MAX_INFLIGHT || ctx.decoded >= PANO_STITCH_MAX_DECODED_AHEAD)
419
+ return false;
379
420
  }
380
421
  }
422
+ return true;
381
423
  }
382
424
  /** 工序 2:把所有「已进 GPU 且还没画」的格子画进各自的 RT —— 一帧画多少格都行(旨②)。
383
425
  * ⛔ 准入只看一件事:`uploaded`。没上传过的纹理当场画 = `texImage2D` 藏进 `render()` 里
@@ -41,6 +41,8 @@
41
41
  *
42
42
  * ── 每帧的三道工序(旨②那两句话逐条落地,顺序固定)────────────────────────────────
43
43
  * 1. `_pumpDownloads()`:补发缺图的下载(每帧 ≤2 张、在册 ≤8、已解码待上传 ≤8)⇒ ⛔ 一批几十条 XHR 一起炸。
44
+ * ⭐ 2026-10-30 需求「加载任务队列」:`_queue` 非空时**只喂队首那一组**,那一组落定才出队开下一组
45
+ * ⇒ 存档前 2 颗全景是一颗一颗下的(第 1 颗下满才轮到第 2 颗),名册按距离点到的排在后面等。
44
46
  * 2. `_drawPass()`:**只画「已经在 GPU 上的」瓦片**,一帧内画多少格都行(旨②「已经载入过的纹理,才可以
45
47
  * 一次性全部渲染到 RT 上面去」)。画完立刻释放那片 ⇒ 显存峰值 = 一格的量。
46
48
  * 3. `_uploadPass()`:**每帧至多一张**新纹理 `initTexture` 进 GPU(旨②「每一帧同样也是只载入一张新纹理
@@ -239,10 +241,21 @@ export interface PanoStitcher {
239
241
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
240
242
  * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
241
243
  * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。
242
- * 新增可选 `front` 参数(2026-10-29 需求):`true` ⇒ 这一批**挪到在册顺序最前** = 每帧那三道工序
243
- * 先取它的料(下载额度「每帧 2 张」先归它)。消费方每帧点一次 ⇒ 换渲染器那次整份作废后,
244
- * 下一帧又把头名抢回来(⛔ 光靠「先入册」做不到,见 `MktMesh.prefetchHeadPanoramas` 的说明)。 */
245
- ensure(urls: string[], layout?: TileLayout, front?: boolean): void;
244
+ * ⚠️ 2026-10-30「加载任务队列」:本方法只负责**入册**(新组追加到在册顺序末尾 ⇒ 排在队列里等),
245
+ * 点名优先的那一路换成交换 `prioritize()`(10-29 那版挂在第三个参 `front` 上的「挪到队首」已并进那里,
246
+ * ⛔ 一处排队只有 `prioritize` 一张尺)。 */
247
+ ensure(urls: string[], layout?: TileLayout): void;
248
+ /**
249
+ * 把某批瓦片登记进**加载任务队列**(2026-10-30 需求,逐字:「依次把前 2 个全景的顺序把瓦片加载任务添加到
250
+ * 任务队列里面,第一个加载完了,就把这个加载任务从任务队列里面去掉,然后加载第二个;后续按距离需要哪个全景
251
+ * 的时候同样添加到队列后面等待加载」)。
252
+ * 语义 = 先 `ensure` 入册(不在册就建组),再把批次键登记进队列**末尾**(已在队列里就不再挪,所以先登记的排前面)。
253
+ * 排队规则见 `_pumpDownloads`:**队首那组落定(32 格全画进 RT,或该格本就缺地址/判死)之前,队列里后面的组
254
+ * 和一切没登记的名册组一片都不发** ⇒ 第 1 颗全景独享全部下载额度、最快出全图。
255
+ * ⚠️ 消费方每帧点一次(`MktMesh.prefetchHeadPanoramas`):预览每开一次都换渲染器,`_switchRenderer` 会把在册组
256
+ * 整份作废 ⇒ 队列跟着清空,下一帧重新登记才能把顺序抢回来(只在 `importData` 里点一次等于白点)。
257
+ */
258
+ prioritize(urls: string[], layout?: TileLayout): void;
246
259
  /**
247
260
  * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
248
261
  * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
@@ -272,6 +285,10 @@ export interface PanoStitcher {
272
285
  export class PanoTileStitcher implements PanoStitcher {
273
286
  /** 批次组(键 = 32 张瓦片的第一张**非空**地址,与消费方同一个 `rtStitchBatchOf`) */
274
287
  private _groups: Map<string, StitchGroup> = new Map();
288
+ /** 加载任务队列(2026-10-30 需求):批次键的**串行带**,队首那组落定才开下一组。
289
+ * 由 `prioritize` 登记(存档里前 2 颗全景),`_pumpDownloads` 每帧出清;`_switchRenderer`/`dispose`/`disposeGroup`
290
+ * 跟着组一起拆。⛔ 这是「谁先下载」的唯一一把尺 —— 在册顺序(`_groups` 的 Map 序)只决定「同不带队列时」的补料次序。 */
291
+ private _queue: string[] = [];
275
292
  /** 瓦片纹理(url → `{tex, uploaded}`)。画进自己那一格就立刻释放 ⇒ 只在册期间占显存/内存。 */
276
293
  private _texs: Map<string, TileRec> = new Map();
277
294
  /** 仍在册的组还需要的地址(迟到的加载回调据此判断「这条已经没人要了,立刻释放」) */
@@ -329,17 +346,14 @@ private _uploadQueued: Set<TileRec> = new Set();
329
346
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
330
347
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
331
348
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
332
- ensure(urls: string[], layout?: TileLayout, front = false): void {
349
+ ensure(urls: string[], layout?: TileLayout): void {
333
350
  const L = layout ?? PANO_TILE_LAYOUT;
334
351
  const maxTiles = tilesPerBatchOf(L);
335
352
  if (!urls || urls.length === 0 || urls.length > maxTiles) return;
353
+ const batch = this._batchKeyOf(urls, L);
354
+ if (this._groups.has(batch)) return;
336
355
  const u = urls.slice();
337
356
  while (u.length < maxTiles) u.push("");
338
- const batch = rtStitchBatchOf(u);
339
- if (this._groups.has(batch)) {
340
- if (front) this._toFront(batch);
341
- return;
342
- }
343
357
  const slotCount = L.slotCount;
344
358
  const cellsPerSlot = cellsPerSlotOf(L);
345
359
  this._groups.set(batch, {
@@ -353,21 +367,39 @@ private _uploadQueued: Set<TileRec> = new Set();
353
367
  drawnCount: Array.from({ length: slotCount }, () => 0),
354
368
  pending: slotCount,
355
369
  });
356
- if (front) this._toFront(batch);
357
370
  this._stamp++;
358
371
  }
359
372
 
360
- /** 把一批挪到在册顺序的**最前**(只动 Map 的顺序,⛔ 不动组里任何东西:RT、已画格子、瓦片账本原样)。
361
- * 已在最前 ⇒ 一次 `keys().next()` 就返回(每帧点名走的就是这条零成本支路)。 */
362
- private _toFront(batch: string): void {
363
- const g = this._groups.get(batch);
364
- if (!g) return;
365
- const head = this._groups.keys().next();
366
- if (!head.done && head.value === batch) return;
367
- const rest = [...this._groups.entries()];
368
- this._groups.clear();
369
- this._groups.set(batch, g);
370
- for (const [k, v] of rest) if (k !== batch) this._groups.set(k, v);
373
+ /** 登记进加载任务队列(语义与排队规则见接口上那一段)。已在队列里 ⇒ 不挪(先登记的仍在前)。 */
374
+ prioritize(urls: string[], layout?: TileLayout): void {
375
+ this.ensure(urls, layout);
376
+ const batch = this._batchKeyOf(urls, layout ?? PANO_TILE_LAYOUT);
377
+ if (!batch || !this._groups.has(batch) || this._queue.includes(batch)) return;
378
+ this._queue.push(batch);
379
+ }
380
+
381
+ /** 地址数组 → 批次键(补齐到满批再取首条非空,与 `ensure` 里同一条尺 ⇒ 两处不会各算各的)。
382
+ * 超限或全空 ⇒ 空串(`ensure` 那一步同样不收,这里也不入队)。 */
383
+ private _batchKeyOf(urls: string[], L: TileLayout): string {
384
+ const maxTiles = tilesPerBatchOf(L);
385
+ if (!urls || urls.length === 0 || urls.length > maxTiles) return "";
386
+ const u = urls.slice(0, maxTiles);
387
+ while (u.length < maxTiles) u.push("");
388
+ return rtStitchBatchOf(u);
389
+ }
390
+
391
+ /** 这一组的每一格要么已经画进 RT,要么根本没有地址 / 地址已判死 ⇒ 该下的都下完了(队列出清判据)。 */
392
+ private _isSettled(g: StitchGroup): boolean {
393
+ for (let slot = 0; slot < g.layout.slotCount; slot++) {
394
+ const ids = g.slotTiles[slot];
395
+ for (let cell = 0; cell < g.cellsPerSlot; cell++) {
396
+ if (g.drawn[slot] & (1 << cell)) continue;
397
+ const url = g.urls[ids[cell]];
398
+ if (!url || this._dead.has(url)) continue;
399
+ return false;
400
+ }
401
+ }
402
+ return true;
371
403
  }
372
404
 
373
405
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
@@ -421,12 +453,14 @@ private _uploadQueued: Set<TileRec> = new Set();
421
453
  this._uploadPass(renderer);
422
454
  }
423
455
 
424
- /** 释放整组(2 张 RT + 该组还没画掉的瓦片纹理) */
456
+ /** 释放一整组(2 张 RT + 该组还没画掉的瓦片纹理);队列里那一记任务跟着撤(组没了还排着 = 队首永远等不到落定) */
425
457
  disposeGroup(batch: string): void {
426
458
  const g = this._groups.get(batch);
427
459
  if (!g) return;
428
460
  this._dropGroupGpu(g);
429
461
  this._groups.delete(batch);
462
+ const qi = this._queue.indexOf(batch);
463
+ if (qi >= 0) this._queue.splice(qi, 1);
430
464
  this._stamp++;
431
465
  }
432
466
 
@@ -451,31 +485,49 @@ private _uploadQueued: Set<TileRec> = new Set();
451
485
  return typeof performance !== "undefined" ? performance.now() : Date.now();
452
486
  }
453
487
 
454
- /** 工序 1:按「槽 0 格 0 → 槽 3 格 15」的顺序补齐还没发起的下载,受三道闸约束(每帧 2 张 /
455
- * 下载中 ≤8 / 已解码待上传 ≤8)⇒ 一片瓦片终身只 `fetch` 一次(在册条目不重复发)。 */
488
+ /** 工序 1:按「槽 0 格 0 → …」的顺序补齐还没发起的下载,受三道闸约束(每帧 2 张 / 下载中 ≤8 /
489
+ * 已解码待上传 ≤8)⇒ 一片瓦片终身只 `fetch` 一次(在册条目不重复发)。
490
+ * ⭐ 2026-10-30「加载任务队列」:队列非空时**只喂队首那一组**,它落定(`_isSettled`)就出队、下一帧轮到下一个;
491
+ * ⇒ 存档里第 1 颗全景独享额度下满,才轮到第 2 颗,之后才轮到名册按距离点到、排在队尾等着的那些全景。
492
+ * 队列为空 ⇒ 退回原来的在册顺序逐组补料(图片平面那一路不入队,照旧按在册顺序)。 */
456
493
  private _pumpDownloads(): void {
457
- let inflight = 0;
458
- let decoded = 0;
494
+ while (this._queue.length > 0) {
495
+ const head = this._groups.get(this._queue[0]);
496
+ if (head && !this._isSettled(head)) break;
497
+ this._queue.shift();
498
+ }
499
+ const ctx = { budget: PANO_STITCH_DOWNLOADS_PER_FRAME, inflight: 0, decoded: 0 };
459
500
  for (const rec of this._texs.values()) {
460
- if (rec.tex) decoded++;
461
- else inflight++;
501
+ if (rec.tex) ctx.decoded++;
502
+ else ctx.inflight++;
503
+ }
504
+ if (this._queue.length > 0) {
505
+ const head = this._groups.get(this._queue[0]);
506
+ if (head) this._issueCells(head, ctx);
507
+ return;
462
508
  }
463
- let budget = PANO_STITCH_DOWNLOADS_PER_FRAME;
464
509
  for (const g of this._groups.values()) {
465
- if (budget <= 0 || inflight >= PANO_STITCH_MAX_INFLIGHT || decoded >= PANO_STITCH_MAX_DECODED_AHEAD) return;
466
- for (let slot = 0; slot < g.layout.slotCount; slot++) {
467
- const ids = g.slotTiles[slot];
468
- for (let cell = 0; cell < g.cellsPerSlot; cell++) {
469
- if (g.drawn[slot] & (1 << cell)) continue;
470
- const url = g.urls[ids[cell]];
471
- if (!url || this._dead.has(url)) continue;
472
- if (this._texs.has(url)) continue;
473
- this._loadTile(url);
474
- inflight++;
475
- if (--budget <= 0 || inflight >= PANO_STITCH_MAX_INFLIGHT || decoded >= PANO_STITCH_MAX_DECODED_AHEAD) return;
476
- }
510
+ if (ctx.budget <= 0 || ctx.inflight >= PANO_STITCH_MAX_INFLIGHT || ctx.decoded >= PANO_STITCH_MAX_DECODED_AHEAD) return;
511
+ if (!this._issueCells(g, ctx)) return;
512
+ }
513
+ }
514
+
515
+ /** 某一组的取料循环:把「还没画、有地址、没判死、没发起过」的格子交给 `_loadTile`。
516
+ * 返回 false = 三道闸里有一道合上了 ⇒ 本帧别再往下看(`ctx` 是连扣的,跨组也接着算)。 */
517
+ private _issueCells(g: StitchGroup, ctx: { budget: number; inflight: number; decoded: number }): boolean {
518
+ for (let slot = 0; slot < g.layout.slotCount; slot++) {
519
+ const ids = g.slotTiles[slot];
520
+ for (let cell = 0; cell < g.cellsPerSlot; cell++) {
521
+ if (g.drawn[slot] & (1 << cell)) continue;
522
+ const url = g.urls[ids[cell]];
523
+ if (!url || this._dead.has(url)) continue;
524
+ if (this._texs.has(url)) continue;
525
+ this._loadTile(url);
526
+ ctx.inflight++;
527
+ if (--ctx.budget <= 0 || ctx.inflight >= PANO_STITCH_MAX_INFLIGHT || ctx.decoded >= PANO_STITCH_MAX_DECODED_AHEAD) return false;
477
528
  }
478
529
  }
530
+ return true;
479
531
  }
480
532
 
481
533
  /** 工序 2:把所有「已进 GPU 且还没画」的格子画进各自的 RT —— 一帧画多少格都行(旨②)。
@@ -1,7 +1,6 @@
1
1
  import type { Matrix4 } from "three";
2
2
  import type { ImagePlaneData } from "./imagePlane";
3
3
  import type { ModelSlotData } from "./modelSlot";
4
- import type { GlassPaneData } from "./glassPane";
5
4
  /**
6
5
  * 单个纹理的运行时信息:url + 矩阵(位置 + 旋转,缩放忽略)。
7
6
  * ⚠️ 这个矩阵记在 MktMesh **自身局部(制图)空间**,与 `wallPoints` 同一空间 —— 不是世界矩阵:
@@ -88,9 +87,4 @@ export interface MktMeshData {
88
87
  images?: ImagePlaneData[];
89
88
  /** 模型槽层(同一道旨):`url` 是 .glb 的相对地址;空串 = 还没上传 ⇒ 显示占位小立方体 */
90
89
  models?: ModelSlotData[];
91
- /**
92
- * 玻璃面层(2026-10-28 需求「正方的面」):正方形、双面、transmission 折射,反射环境取运行时第一个全景。
93
- * ⛔ 这一层不落任何素材地址 ⇒ 既不过 `stripAssetHost` 也不过 `absAssetUrl`,只有位置/旋转/边长。
94
- */
95
- glass?: GlassPaneData[];
96
90
  }
package/MktMesh/types.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  import type { Matrix4 } from "three";
9
9
  import type { ImagePlaneData } from "./imagePlane";
10
10
  import type { ModelSlotData } from "./modelSlot";
11
- import type { GlassPaneData } from "./glassPane";
12
11
 
13
12
  /**
14
13
  * 单个纹理的运行时信息:url + 矩阵(位置 + 旋转,缩放忽略)。
@@ -100,9 +99,4 @@ export interface MktMeshData {
100
99
  images?: ImagePlaneData[];
101
100
  /** 模型槽层(同一道旨):`url` 是 .glb 的相对地址;空串 = 还没上传 ⇒ 显示占位小立方体 */
102
101
  models?: ModelSlotData[];
103
- /**
104
- * 玻璃面层(2026-10-28 需求「正方的面」):正方形、双面、transmission 折射,反射环境取运行时第一个全景。
105
- * ⛔ 这一层不落任何素材地址 ⇒ 既不过 `stripAssetHost` 也不过 `absAssetUrl`,只有位置/旋转/边长。
106
- */
107
- glass?: GlassPaneData[];
108
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-base",
3
- "version": "2.0.193",
3
+ "version": "2.0.195",
4
4
  "description": "A Three.js extension library providing common 3D components, animations, materials, and utilities",
5
5
  "author": "xiaosihan",
6
6
  "publishConfig": {
@@ -47,11 +47,6 @@
47
47
  "import": "./MktMesh/modelSlot.js",
48
48
  "require": "./MktMesh/modelSlot.js"
49
49
  },
50
- "./MktMesh/glassPane": {
51
- "types": "./MktMesh/glassPane.d.ts",
52
- "import": "./MktMesh/glassPane.js",
53
- "require": "./MktMesh/glassPane.js"
54
- },
55
50
  "./MktMesh/boxLine2": {
56
51
  "types": "./MktMesh/boxLine2.d.ts",
57
52
  "import": "./MktMesh/boxLine2.js",
@@ -1,43 +0,0 @@
1
- import { Mesh, MeshPhysicalMaterial, PlaneGeometry, Texture } from "three";
2
- /** 玻璃面的持久化数据(进 `MktMesh.exportData` 的 `glass` 键 / `importData` 还原) */
3
- export interface GlassPaneData {
4
- name?: string;
5
- position: {
6
- x: number;
7
- y: number;
8
- z: number;
9
- };
10
- rotation: {
11
- x: number;
12
- y: number;
13
- z: number;
14
- };
15
- /** 正方形边长(米):宽 = 高 = 本值,⛔ 不拆成两档 */
16
- size: number;
17
- }
18
- export declare class GlassPane extends Mesh<PlaneGeometry, MeshPhysicalMaterial> {
19
- private _originalColor;
20
- /** 反射环境纹理的身份(由 `MktMesh` 注入,本类只持有引用、⛔ 从不 dispose 它 —— 那张按地址缓存在库里) */
21
- private _envTex;
22
- constructor(name?: string);
23
- get paneSize(): number;
24
- /** 换正方形边长(宽高等比跟着走:几何只有这一把尺) */
25
- setPaneSize(size: number): void;
26
- /**
27
- * 绑/摘反射环境纹理(`null` = 还没有可用的环境源)。
28
- * ⚠️ 判等在前:`material.needsUpdate = true` 会涨材质版本 ⇒ 程序要重编译。库里给同一批玻璃面注入的是
29
- * **同一个 Texture 对象**(反射源只下载一次),逐颗重复赋同一张时必须早退,不然就是 N 次无谓重编译。
30
- * ⚠️ 传进来的纹理必须已标 `EquirectangularReflectionMapping`:three 的 `WebGLCubeUVMaps` 只认那个标记才会
31
- * 自动把它卷积成立方 UV(PMREM);直接给一张普通 UV 纹理(例如全景拼合槽的那两张 RT)采出来是花屏。
32
- */
33
- setEnvironmentTexture(tex: Texture | null): void;
34
- highlight(): void;
35
- restoreColor(): void;
36
- toData(): GlassPaneData;
37
- applyData(d: GlassPaneData): void;
38
- /** 只释放自己的几何与材质;反射纹理归 `MktMesh` 管(多颗共用一张)。 */
39
- dispose(): void;
40
- }
41
- export declare const GLASS_PANE_DEFAULTS: {
42
- size: number;
43
- };