three-base 2.0.197 → 2.0.199

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.
@@ -5,6 +5,7 @@ import { type PanoStitcher } from "./panoTileStitcher";
5
5
  import type { MktMeshData, MktTextureInfo, WallPoint, WallSide } from "./types";
6
6
  import { ImagePlane, type ImagePlaneData } from "./imagePlane";
7
7
  import { ModelSlot, type ModelSlotData } from "./modelSlot";
8
+ import { MktResourceManager } from "./mktResManager";
8
9
  export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
9
10
  /** 纹理列表(每个纹理的 url + 世界矩阵) */
10
11
  textures: MktTextureInfo[];
@@ -121,6 +122,14 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
121
122
  /** 当前全景纹理形态(只读)。消费方(预览立方体等自持材质的路径)据此算 `mktPanoTiledFlag`,
122
123
  * 必须与基类喂纹理处用同一个值,否则标记与地址不同源 ⇒ 整图被按 1/2 横向铺满球面(10-06 晚起纵向不分块,四槽时代是 1/4)。 */
123
124
  get panoTexMode(): PanoTexMode;
125
+ /** 优先加载的全景个数(存档 `textures` 的前 n 项):**-1 = 全部、0 = 一个都不提前、n = 前 n 个**(2026-11-05 需求)。
126
+ * 默认取 `PANO_IMPORT_PRIORITY_DEFAULT`(= -1,同日需求「不管有没有显示出来的全景图都应该默认全部加载」);
127
+ * 宿主想收窄,在渲染循环跑起来之前 `setPanoPriorityCount(4)` 一行即可
128
+ * (与 `_ossSize`、`_panoTexMode` 同一套注入路径 ⇒ 编辑器与预览各自的配置各自推下来)。
129
+ * ⚠️ 这一档只定**下载与拼合的次序**;隐藏期里谁来推那一拍 = `preRender(renderer)`(需求第 1、2 点)。 */
130
+ private _panoPriorityCount;
131
+ /** 当前优先加载个数(只读,回显用)。 */
132
+ get panoPriorityCount(): number;
124
133
  private _tmpV3;
125
134
  /**
126
135
  * 图片平面层(⭐ 10-26 旨入库):对象挂在**本 MktMesh 的子树**里,⛔ 不是场景根
@@ -162,6 +171,14 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
162
171
  * 否则它会在轮到自身那一帧被写回可见(`boolean` 只有一个位,放行那里分不出这笔是谁写的)。
163
172
  */
164
173
  private _revealPending;
174
+ /**
175
+ * ⭐ 2026-11-05 需求「专门写一个资源管理器来管理,每个 mktmesh 都有自己的一个资源管理器」:本对象一份。
176
+ * 它不下载、不解码、不上传 ⇒ 全部经下面那几只手点本对象上**现成**的入口(拼合器 `pump` / `prefetchHeadPanoramas` /
177
+ * `feedImagePlaneTiles` / 隐藏期预热),只多管两件事:隐藏期由谁来推那一拍、以及「哪一档真画进过显存」这本账。
178
+ */
179
+ private _res;
180
+ /** 本对象的资源管理器(只读)。宿主只管调 `preRender`,需要看账本时才用它。 */
181
+ get resManager(): MktResourceManager;
165
182
  /**
166
183
  * 隐藏期 GPU 预热的在册清单(元素 = 一个「会进渲染列表的物件」,即它的材质组合 × 几何)。
167
184
  * null = 没在预热(未启动 / 已跑完 / `stopGpuPrewarm` / `dispose` 四种都是它)。
@@ -338,6 +355,11 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
338
355
  * 下一帧 `feedFaceMapping` 里共用的那份 binder 见「槽内现有形态 ≠ 目标形态」自动走整档判齐、一次换绑并同帧翻标记。
339
356
  * 编辑器 MktDevMesh 走的是自己那路(同一个 binder、同一份策略),本方法对它只是记下形态。 */
340
357
  setPanoTexMode(mode: PanoTexMode): void;
358
+ /** 设置优先加载的全景个数:-1 = 全部 / 0 = 不提前 / n = 存档前 n 个(2026-11-05 需求)。
359
+ * 收口:负数(含 -2、NaN 一类)一律归到 -1「全加载」,正数向下取整;非有限值回落默认档。
360
+ * 改档⛔ 需要重新 `importData` —— 名册每帧重排,下一帧 `prefetchHeadPanoramas` 直接用新数点名
361
+ * (`prioritize` 幂等 ⇒ 已在队列里的那批不重复投喂,也不会把已出队的旧批再塞回去)。 */
362
+ setPanoPriorityCount(count: number): void;
341
363
  /**
342
364
  * 记下「当前激活(名册 0 号槽 = 中心)是第几个全景」。
343
365
  * ⭐ 皇上旨 2026-10-03 旨②:预览立方体已与墙**共用同一份材质**,原来那句「把中心那几张抄给立方体单全景材质」
@@ -413,8 +435,27 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
413
435
  * 表空了它就是一句 `length` 判断,⛔ 不需要再像旧写法那样把钩子置 null(钩子上还挂着全景投影那三件事)。
414
436
  * ⚠️ 这里写的 `visible` 只针对**自己压进表的那几颗** ⇒ 表外的对象(单颗新增、已 dispose、被 `releaseReveal`
415
437
  * 摘牌的)一律不碰;要在放行前接管某颗,调 `releaseReveal(o)`。
438
+ * ⭐ 2026-11-05 需求第 4 点「只已经预载入的资源才显示出来」:内容落定(`o.loaded`)之后还要问一句资源管理器
439
+ * 「这一档画进显存了吗」—— 只有宿主调过 `preRender` 且预热还挂着牌时才拦,摘牌 / 没调过 ⇒ 一律放行(⛔ 不许拦出永久黑屏)。
416
440
  */
417
441
  tickProgressiveReveal(): void;
442
+ /**
443
+ * ⭐ 2026-11-05 需求第 1 点逐字:「需要提供一个 `preRender` 方法, 传入一个渲染器对象, 然后在这个方法里面使用
444
+ * 渲染来预载入已经准备好的资源到 GPU」。隐藏期(`visible = false`)里 three 遍历第一句就把本对象连同子树跳过
445
+ * ⇒ `onBeforeRender` 一次都不触发,全景瓦片不下载不拼合、平面不绑、GLB 与墙的 program 不编不传;
446
+ * 本方法就是那句「藏起来的时候替它跑一遍渲染循环里的推进」,交进来的渲染器同时供拼合器与预热用。
447
+ *
448
+ * 一次点齐三族(次序固定,与 `onBeforeRender` 里同一相对位置):
449
+ * ① 全景 —— `stitcher.pump(renderer)` 逐 tick 走「补下载 → 画已进显存的格 → 传一张新的」,
450
+ * 额度由 `panoPriorityCount` 点的批排在前(默认 -1 = 整份存档,需求第 2 点);
451
+ * ② 平面图 —— `feedImagePlaneTiles()` 一次把整层地址交给下载、绑定一帧一颗(需求第 3 点:这两族本来就全量发起);
452
+ * ③ GLB / 平面 / 墙 —— 下面的 `startGpuPrewarm` 那一族逐 tick 画一件进 1×1 目标(program 编译 + 顶点缓冲首传)。
453
+ *
454
+ * 幂等 ⇒ 隐藏期里每帧点一次、或只在载入后点一次都行;已排进队列的那条链不会被重复起。
455
+ * 本对象一旦 `visible`(进厅),链自己停 ⇒ 之后交回 `onBeforeRender` 每帧那一路,⛔ 不抢同一帧的额度。
456
+ * ⛔ 不调本方法的宿主(发布页直接亮墙、编辑器画布)行为与改造前逐字一致:显示闸只在 `preRender` 点过之后才生效。
457
+ */
458
+ preRender(renderer: WebGLRenderer): void;
418
459
  /**
419
460
  * 开始隐藏期预热:把「进厅第一帧要现编的那些 program + 要第一次上传的那些几何/纹理」提前跑掉,一帧只放**一件**。
420
461
  *
@@ -499,11 +540,12 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
499
540
  * @param onModelLoad 每只 GLB 落地时回调一次(可选)——按需渲染的宿主(编辑器)拿它标脏;每帧真画的预览不传 */
500
541
  importData(data: MktMeshData, projName?: string, onModelLoad?: () => void): void;
501
542
  /**
502
- * 把**存档里前 `PANO_IMPORT_PRIORITY` 个全景**按存档顺序登记进拼合器的**加载任务队列**
543
+ * 把**存档里前 `panoPriorityCount` 个全景**(-1 = 整份全加载)按存档顺序登记进拼合器的**加载任务队列**
503
544
  *(2026-10-29 需求「优先加载前 2 个全景素材」+ 2026-10-30 需求逐字:「给 mktmesh 内部加一个加载任务队列,
504
545
  * 把模型和平面图排在最前面,然后按照顺序,依次把前 2 个全景的顺序把瓦片加载任务添加到任务队列里面,
505
546
  * 第一个加载完了,就把这个加载任务从任务队列里面去掉,然后加载第二个;
506
- * 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」)。
547
+ * 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」
548
+ * + 2026-11-05 需求「提前加载多少个全景改由配置决定,-1 全加载 / 0 不加载 / n 前 n 个,默认 4」)。
507
549
  * 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
508
550
  *(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
509
551
  *
@@ -512,21 +554,23 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
512
554
  * (GLB 逐只 `loadModel` 不 throttle;平面那 4 片一次性 `_imgTileLoader.preload`),
513
555
  * 而全景瓦片最早也要等下一帧的 `pump` 才发第一片 ⇒ 发起次序天然是「模型 → 平面图 → 全景」,
514
556
  * ⛔ 这里不再把它们塞进队列重复管一遍(塞了就是第二把尺)。
515
- * ② 本方法把存档 0 号、1 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
557
+ * ② 本方法把存档 0 号起这 n 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
516
558
  * 0 号 32 格全落定(画进 RT,或该格本就缺地址/判死)才出队、1 号才开始发第一片。
517
559
  * 比 10-29 那版(只是把两批挪到在册顺序前面、额度仍被两批分摊)更硬:**第 1 颗独享额度,最快出全图**。
560
+ * n = -1(全加载)时这一条变成「整份存档按存档顺序串行」⇒ 距离名册插不到前面,出清速度取决于总片数。
518
561
  * ③ 名册按距离点到的其他全景走 `feedFaceMapping` 里那句 `ensure(t.tiles)`(不带队列)⇒ 只入册、
519
562
  * 排在队列后面等,队列空了才按在册顺序补料(现行口径不变)。
520
563
  * ⚠️⚠️ 为什么每帧点而⛔ 只在 `importData` 里点一记:预览每开一次都换渲染器,`_switchRenderer` 把在册组
521
564
  * 整份作废(队列里的键也在同一刻失效);只在载入那次点名等于白点。本方法点在 `pump` 之后、名册逐槽
522
- * `ensure` 之前 ⇒ 每帧把这两记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以 0 号恒在 1 号前)。
565
+ * `ensure` 之前 ⇒ 每帧把这几记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以存档次序恒不变)。
523
566
  * ⚠️ 代价如实记:
524
- * · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前两颗的那面墙会多灰一会儿。
567
+ * · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前 n 颗的那面墙会多灰一会儿。
525
568
  * · 一颗「永远下不完」的瓦片(下载失败还没累计到判死)会压住队列;判死阈值是解码失败 3 次
526
- * (`_dead`),退避重试期间队首不落定 ⇒ 第 2 颗跟着延后。真实数据里缺片是「地址为空/404」,
569
+ * (`_dead`),退避重试期间队首不落定 ⇒ 后面几颗跟着延后。真实数据里缺片是「地址为空/404」,
527
570
  * 404 走三次即判死、那一格拼合涂灰 ⇒ 队列照样出清。
528
- * · 这两颗全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
529
- * (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这两个景点迟早要建。
571
+ * · 这批全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
572
+ * (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这些景点迟早要建;
573
+ * ⚠️ n = -1(全加载)时这一条按存档全景总数放大,显存与在途请求都跟着涨。
530
574
  * ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
531
575
  * 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
532
576
  * 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
@@ -49,11 +49,18 @@ import { stripAssetHost, absAssetUrl } from "./assetUrl";
49
49
  // 原先各住在本站两个渲染器里一份 ⇒ 下游直接用包里的 `MktMesh` 时,存档里的 `images`/`models` 两键没人读。
50
50
  // 入库之后它们挂在 `MktMesh` **自己的子树**下,与墙同一棵变换 ⇒ 预览那三根 transform 滑杆搬得动整份展品。
51
51
  import { ImagePlane } from "./imagePlane";
52
- import { ModelSlot } from "./modelSlot";
52
+ import { ModelSlot, glbFileName } from "./modelSlot";
53
53
  import { ImagePlaneTileLoader } from "./imageTileLoader";
54
- /** 载入 JSON 时**优先**加载几个全景的素材(2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」)。
55
- * 口径 = 存档 `textures` 数组的前 n 项;其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。 */
56
- const PANO_IMPORT_PRIORITY = 2;
54
+ // ⭐ 用户需求 2026-11-05「专门写一个资源管理器来管理, 每个 mktmesh 都有自己的一个资源管理器来管理」:
55
+ // 隐藏期(`visible = false` ⇒ `onBeforeRender` 一次都不触发)里那三族资源的**驱动次序**与**是否已进显存**这本账。
56
+ import { MktResourceManager } from "./mktResManager";
57
+ /** 载入 JSON 时**优先**加载几个全景的素材的**默认档**(数值只写这一处)。
58
+ * 口径 = 存档 `textures` 数组的前 n 项(n = -1 视为整份全加载);其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。
59
+ * 2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」立的是 2;
60
+ * 2026-11-05 需求逐字「现在要默认把全景全部都要默认加载, 现在需要通过配置来设置, 提前加载多少个全景 -1 就是全加载 0就是不加载 1就是加载个 2就提前加载2个 一次类推, 默认加载前4个」⇒ 换成可配置的 4;
61
+ * 同日需求逐字「panoPriorityCount 设置为 -1, 不管有没有显示出来的全景图都应该默认全部加载, 等待渲染器的预载入」⇒ 默认档落 **-1(整份全加载)**。
62
+ * ⚠️ 这一档管的是**下载与拼合的次序**;「亮出来之前先把它推进显存」是 `preRender(renderer)` 那一手(见 `./mktResManager`)。 */
63
+ const PANO_IMPORT_PRIORITY_DEFAULT = -1;
57
64
  /** 隐藏期预热那一次绘制用的镜头(模块级共用:它只是个「有合法的投影矩阵可读」的载体,永不动 ⇒ ⛔ 不必每实例造一份)。 */
58
65
  const GPU_WARM_CAMERA = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
59
66
  /**
@@ -193,6 +200,16 @@ export default class MktMesh extends Mesh {
193
200
  get panoTexMode() {
194
201
  return this._panoTexMode;
195
202
  }
203
+ /** 优先加载的全景个数(存档 `textures` 的前 n 项):**-1 = 全部、0 = 一个都不提前、n = 前 n 个**(2026-11-05 需求)。
204
+ * 默认取 `PANO_IMPORT_PRIORITY_DEFAULT`(= -1,同日需求「不管有没有显示出来的全景图都应该默认全部加载」);
205
+ * 宿主想收窄,在渲染循环跑起来之前 `setPanoPriorityCount(4)` 一行即可
206
+ * (与 `_ossSize`、`_panoTexMode` 同一套注入路径 ⇒ 编辑器与预览各自的配置各自推下来)。
207
+ * ⚠️ 这一档只定**下载与拼合的次序**;隐藏期里谁来推那一拍 = `preRender(renderer)`(需求第 1、2 点)。 */
208
+ _panoPriorityCount = PANO_IMPORT_PRIORITY_DEFAULT;
209
+ /** 当前优先加载个数(只读,回显用)。 */
210
+ get panoPriorityCount() {
211
+ return this._panoPriorityCount;
212
+ }
196
213
  // 复用临时对象(避免每帧 new,降低 GC 压力)
197
214
  _tmpV3 = new Vector3();
198
215
  /**
@@ -235,6 +252,25 @@ export default class MktMesh extends Mesh {
235
252
  * 否则它会在轮到自身那一帧被写回可见(`boolean` 只有一个位,放行那里分不出这笔是谁写的)。
236
253
  */
237
254
  _revealPending = [];
255
+ /**
256
+ * ⭐ 2026-11-05 需求「专门写一个资源管理器来管理,每个 mktmesh 都有自己的一个资源管理器」:本对象一份。
257
+ * 它不下载、不解码、不上传 ⇒ 全部经下面那几只手点本对象上**现成**的入口(拼合器 `pump` / `prefetchHeadPanoramas` /
258
+ * `feedImagePlaneTiles` / 隐藏期预热),只多管两件事:隐藏期由谁来推那一拍、以及「哪一档真画进过显存」这本账。
259
+ */
260
+ _res = new MktResourceManager({
261
+ prewarm: (renderer) => this.startGpuPrewarm(renderer),
262
+ stopPrewarm: () => this.stopGpuPrewarm(),
263
+ // 挂着牌 = 还没摘牌且还没亮出来 ⇒ 与 `_topUpGpuPrewarm` 用的同一颗尺(⛔ 不自立第二道闸)
264
+ prewarming: () => this._warmSeen !== null && !this.visible,
265
+ shown: () => this.visible,
266
+ pumpPano: (renderer) => this._stitcher.pump(renderer),
267
+ prefetchPano: () => this.prefetchHeadPanoramas(),
268
+ feedPlanes: () => this.feedImagePlaneTiles(),
269
+ });
270
+ /** 本对象的资源管理器(只读)。宿主只管调 `preRender`,需要看账本时才用它。 */
271
+ get resManager() {
272
+ return this._res;
273
+ }
238
274
  /**
239
275
  * 隐藏期 GPU 预热的在册清单(元素 = 一个「会进渲染列表的物件」,即它的材质组合 × 几何)。
240
276
  * null = 没在预热(未启动 / 已跑完 / `stopGpuPrewarm` / `dispose` 四种都是它)。
@@ -284,6 +320,8 @@ export default class MktMesh extends Mesh {
284
320
  // `updateTexture`(它里面那记 `pump` 是「第一次拿到 renderer ⇒ 在册组整份作废」的现场)之后、
285
321
  // `feedFaceMapping`(名册逐槽入册)之前 —— 顺序倒过来就是「名册在前、我们垫底」= 静默失效。
286
322
  // 编辑态那份在 `MktDevMesh` 的宿主循环里照同一相对位置点(⛔ 别在这儿加第三处)。
323
+ // 2026-11-05 需求:这一批点几颗改由 `panoPriorityCount` 定(默认 -1 = 存档全部)⇒ 本句不动;
324
+ // 同一批在**隐藏期**由 `preRender(renderer)` 逐 tick 推(本钩子在那时一次都不触发)。
287
325
  this.prefetchHeadPanoramas();
288
326
  this.feedFaceMapping();
289
327
  this.tickProgressiveReveal();
@@ -1069,6 +1107,18 @@ export default class MktMesh extends Mesh {
1069
1107
  return;
1070
1108
  this._panoTexMode = next;
1071
1109
  }
1110
+ /** 设置优先加载的全景个数:-1 = 全部 / 0 = 不提前 / n = 存档前 n 个(2026-11-05 需求)。
1111
+ * 收口:负数(含 -2、NaN 一类)一律归到 -1「全加载」,正数向下取整;非有限值回落默认档。
1112
+ * 改档⛔ 需要重新 `importData` —— 名册每帧重排,下一帧 `prefetchHeadPanoramas` 直接用新数点名
1113
+ * (`prioritize` 幂等 ⇒ 已在队列里的那批不重复投喂,也不会把已出队的旧批再塞回去)。 */
1114
+ setPanoPriorityCount(count) {
1115
+ if (!Number.isFinite(count))
1116
+ this._panoPriorityCount = PANO_IMPORT_PRIORITY_DEFAULT;
1117
+ else {
1118
+ const n = Math.floor(count);
1119
+ this._panoPriorityCount = n < 0 ? -1 : n;
1120
+ }
1121
+ }
1072
1122
  /**
1073
1123
  * 记下「当前激活(名册 0 号槽 = 中心)是第几个全景」。
1074
1124
  * ⭐ 皇上旨 2026-10-03 旨②:预览立方体已与墙**共用同一份材质**,原来那句「把中心那几张抄给立方体单全景材质」
@@ -1189,6 +1239,9 @@ export default class MktMesh extends Mesh {
1189
1239
  ms.dispose();
1190
1240
  }
1191
1241
  this.models = [];
1242
+ // 用户 2026-11-04「日志少打印点吧」⇒ 这一路(进厅 / 切项目 / 预览的整层重建)几十颗⛔ 不再一颗一行:
1243
+ // 名单先攒着,循环结束打**一条**(文件名口径与 `setModelUrl` 那一记共用 `glbFileName`)⇒ 一颗不落、一行到位。
1244
+ const glbNames = [];
1192
1245
  for (const d of datas) {
1193
1246
  const ms = new ModelSlot(d.name);
1194
1247
  ms.applyData(d);
@@ -1200,9 +1253,12 @@ export default class MktMesh extends Mesh {
1200
1253
  //(⚠️ 与旧的「GLB 在路上时这一槽露出占位方块」不同:整层重建这一路一颗都不露,直到轮到自己亮)
1201
1254
  ms.visible = false;
1202
1255
  this._revealPending.push(ms);
1256
+ glbNames.push(glbFileName(d.url));
1203
1257
  void ms.loadModel(d.url).then(() => this._queueModelMount(ms, onEachLoad));
1204
1258
  }
1205
1259
  }
1260
+ if (glbNames.length > 0)
1261
+ console.log(`[MktMesh] 整层重建加载 ${glbNames.length} 个 glb:${glbNames.join("、")}`);
1206
1262
  }
1207
1263
  /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
1208
1264
  _queueModelMount(ms, onEachLoad) {
@@ -1299,6 +1355,8 @@ export default class MktMesh extends Mesh {
1299
1355
  * 表空了它就是一句 `length` 判断,⛔ 不需要再像旧写法那样把钩子置 null(钩子上还挂着全景投影那三件事)。
1300
1356
  * ⚠️ 这里写的 `visible` 只针对**自己压进表的那几颗** ⇒ 表外的对象(单颗新增、已 dispose、被 `releaseReveal`
1301
1357
  * 摘牌的)一律不碰;要在放行前接管某颗,调 `releaseReveal(o)`。
1358
+ * ⭐ 2026-11-05 需求第 4 点「只已经预载入的资源才显示出来」:内容落定(`o.loaded`)之后还要问一句资源管理器
1359
+ * 「这一档画进显存了吗」—— 只有宿主调过 `preRender` 且预热还挂着牌时才拦,摘牌 / 没调过 ⇒ 一律放行(⛔ 不许拦出永久黑屏)。
1302
1360
  */
1303
1361
  tickProgressiveReveal() {
1304
1362
  const q = this._revealPending;
@@ -1306,12 +1364,33 @@ export default class MktMesh extends Mesh {
1306
1364
  const o = q[i];
1307
1365
  if (!o.loaded)
1308
1366
  continue; // 内容还在路上 ⇒ 这一颗继续压着,先看后面已经落定的
1367
+ if (this._res.holdsReveal(o))
1368
+ continue; // 内容到了但还没进显存 ⇒ 等 `preRender` 那一拍画过它
1309
1369
  q.splice(i, 1);
1310
1370
  o.visible = true;
1311
1371
  return;
1312
1372
  }
1313
1373
  }
1314
1374
  // ─────────────────── 隐藏期 GPU 预热(2026-11-01 需求:进厅那一下不要再现编着色器) ───────────────────
1375
+ /**
1376
+ * ⭐ 2026-11-05 需求第 1 点逐字:「需要提供一个 `preRender` 方法, 传入一个渲染器对象, 然后在这个方法里面使用
1377
+ * 渲染来预载入已经准备好的资源到 GPU」。隐藏期(`visible = false`)里 three 遍历第一句就把本对象连同子树跳过
1378
+ * ⇒ `onBeforeRender` 一次都不触发,全景瓦片不下载不拼合、平面不绑、GLB 与墙的 program 不编不传;
1379
+ * 本方法就是那句「藏起来的时候替它跑一遍渲染循环里的推进」,交进来的渲染器同时供拼合器与预热用。
1380
+ *
1381
+ * 一次点齐三族(次序固定,与 `onBeforeRender` 里同一相对位置):
1382
+ * ① 全景 —— `stitcher.pump(renderer)` 逐 tick 走「补下载 → 画已进显存的格 → 传一张新的」,
1383
+ * 额度由 `panoPriorityCount` 点的批排在前(默认 -1 = 整份存档,需求第 2 点);
1384
+ * ② 平面图 —— `feedImagePlaneTiles()` 一次把整层地址交给下载、绑定一帧一颗(需求第 3 点:这两族本来就全量发起);
1385
+ * ③ GLB / 平面 / 墙 —— 下面的 `startGpuPrewarm` 那一族逐 tick 画一件进 1×1 目标(program 编译 + 顶点缓冲首传)。
1386
+ *
1387
+ * 幂等 ⇒ 隐藏期里每帧点一次、或只在载入后点一次都行;已排进队列的那条链不会被重复起。
1388
+ * 本对象一旦 `visible`(进厅),链自己停 ⇒ 之后交回 `onBeforeRender` 每帧那一路,⛔ 不抢同一帧的额度。
1389
+ * ⛔ 不调本方法的宿主(发布页直接亮墙、编辑器画布)行为与改造前逐字一致:显示闸只在 `preRender` 点过之后才生效。
1390
+ */
1391
+ preRender(renderer) {
1392
+ this._res.preRender(renderer);
1393
+ }
1315
1394
  /**
1316
1395
  * 开始隐藏期预热:把「进厅第一帧要现编的那些 program + 要第一次上传的那些几何/纹理」提前跑掉,一帧只放**一件**。
1317
1396
  *
@@ -1413,6 +1492,9 @@ export default class MktMesh extends Mesh {
1413
1492
  return;
1414
1493
  seen.add(key);
1415
1494
  items.push({ src: o, m: mesh.material, g });
1495
+ // ⭐ 2026-11-05 需求第 4 点:把「这一颗属于哪一档」交给资源管理器记账(键的算式仍只在这里这一处),
1496
+ // 等 `_runGpuWarmStep` 真画过它再翻成「已进显存」⇒ `tickProgressiveReveal` 才放行。
1497
+ this._res.noteWarmKey(o, key);
1416
1498
  added++;
1417
1499
  });
1418
1500
  return added;
@@ -1463,6 +1545,9 @@ export default class MktMesh extends Mesh {
1463
1545
  renderer.setRenderTarget(this._warmRt);
1464
1546
  renderer.render(scene, GPU_WARM_CAMERA);
1465
1547
  renderer.setRenderTarget(prevTarget);
1548
+ // ⭐ 2026-11-05 需求第 4 点:这一档刚刚真画进显存(program 编了、顶点缓冲与纹理首传也做了)⇒ 记上「已常驻」,
1549
+ // `tickProgressiveReveal` 那一拍才许把对应那颗亮出来(同一档的第二、第三颗跟着一起放行)。
1550
+ this._res.markWarmDrawn(it.src);
1466
1551
  this._scheduleGpuWarmStep(renderer);
1467
1552
  }
1468
1553
  /**
@@ -1641,11 +1726,12 @@ export default class MktMesh extends Mesh {
1641
1726
  this.setModels((data.models ?? []).map((d) => ({ ...d, url: d.url ? absAssetUrl(d.url, pn) : "" })), onModelLoad);
1642
1727
  }
1643
1728
  /**
1644
- * 把**存档里前 `PANO_IMPORT_PRIORITY` 个全景**按存档顺序登记进拼合器的**加载任务队列**
1729
+ * 把**存档里前 `panoPriorityCount` 个全景**(-1 = 整份全加载)按存档顺序登记进拼合器的**加载任务队列**
1645
1730
  *(2026-10-29 需求「优先加载前 2 个全景素材」+ 2026-10-30 需求逐字:「给 mktmesh 内部加一个加载任务队列,
1646
1731
  * 把模型和平面图排在最前面,然后按照顺序,依次把前 2 个全景的顺序把瓦片加载任务添加到任务队列里面,
1647
1732
  * 第一个加载完了,就把这个加载任务从任务队列里面去掉,然后加载第二个;
1648
- * 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」)。
1733
+ * 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」
1734
+ * + 2026-11-05 需求「提前加载多少个全景改由配置决定,-1 全加载 / 0 不加载 / n 前 n 个,默认 4」)。
1649
1735
  * 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
1650
1736
  *(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
1651
1737
  *
@@ -1654,27 +1740,31 @@ export default class MktMesh extends Mesh {
1654
1740
  * (GLB 逐只 `loadModel` 不 throttle;平面那 4 片一次性 `_imgTileLoader.preload`),
1655
1741
  * 而全景瓦片最早也要等下一帧的 `pump` 才发第一片 ⇒ 发起次序天然是「模型 → 平面图 → 全景」,
1656
1742
  * ⛔ 这里不再把它们塞进队列重复管一遍(塞了就是第二把尺)。
1657
- * ② 本方法把存档 0 号、1 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
1743
+ * ② 本方法把存档 0 号起这 n 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
1658
1744
  * 0 号 32 格全落定(画进 RT,或该格本就缺地址/判死)才出队、1 号才开始发第一片。
1659
1745
  * 比 10-29 那版(只是把两批挪到在册顺序前面、额度仍被两批分摊)更硬:**第 1 颗独享额度,最快出全图**。
1746
+ * n = -1(全加载)时这一条变成「整份存档按存档顺序串行」⇒ 距离名册插不到前面,出清速度取决于总片数。
1660
1747
  * ③ 名册按距离点到的其他全景走 `feedFaceMapping` 里那句 `ensure(t.tiles)`(不带队列)⇒ 只入册、
1661
1748
  * 排在队列后面等,队列空了才按在册顺序补料(现行口径不变)。
1662
1749
  * ⚠️⚠️ 为什么每帧点而⛔ 只在 `importData` 里点一记:预览每开一次都换渲染器,`_switchRenderer` 把在册组
1663
1750
  * 整份作废(队列里的键也在同一刻失效);只在载入那次点名等于白点。本方法点在 `pump` 之后、名册逐槽
1664
- * `ensure` 之前 ⇒ 每帧把这两记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以 0 号恒在 1 号前)。
1751
+ * `ensure` 之前 ⇒ 每帧把这几记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以存档次序恒不变)。
1665
1752
  * ⚠️ 代价如实记:
1666
- * · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前两颗的那面墙会多灰一会儿。
1753
+ * · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前 n 颗的那面墙会多灰一会儿。
1667
1754
  * · 一颗「永远下不完」的瓦片(下载失败还没累计到判死)会压住队列;判死阈值是解码失败 3 次
1668
- * (`_dead`),退避重试期间队首不落定 ⇒ 第 2 颗跟着延后。真实数据里缺片是「地址为空/404」,
1755
+ * (`_dead`),退避重试期间队首不落定 ⇒ 后面几颗跟着延后。真实数据里缺片是「地址为空/404」,
1669
1756
  * 404 走三次即判死、那一格拼合涂灰 ⇒ 队列照样出清。
1670
- * · 这两颗全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
1671
- * (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这两个景点迟早要建。
1757
+ * · 这批全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
1758
+ * (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这些景点迟早要建;
1759
+ * ⚠️ n = -1(全加载)时这一条按存档全景总数放大,显存与在途请求都跟着涨。
1672
1760
  * ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
1673
1761
  * 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
1674
1762
  * 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
1675
1763
  */
1676
1764
  prefetchHeadPanoramas() {
1677
- const n = Math.min(PANO_IMPORT_PRIORITY, this.textures.length);
1765
+ const total = this.textures.length;
1766
+ // -1 = 整份;0 = 一个都不提前(这一句直接空转,其余全景仍按距离名册排队 = 回到 10-29 之前的行为)
1767
+ const n = this._panoPriorityCount < 0 ? total : Math.min(this._panoPriorityCount, total);
1678
1768
  // 正着点:0 号先入队 ⇒ 队首是它,落定后才轮到 1 号(`prioritize` 对已在队列里的键不挪位置)
1679
1769
  for (let i = 0; i < n; i++) {
1680
1770
  const info = this.textures[i];
@@ -1716,6 +1806,7 @@ export default class MktMesh extends Mesh {
1716
1806
  this._revealPending = [];
1717
1807
  // 隐藏期预热:清单、去重表与小屋子那套临时资源随本对象一起退场(那张 1×1 渲染目标是本类**唯一**自建的 RT)。
1718
1808
  // 走 `stopGpuPrewarm` 而不是 `_finishGpuPrewarm`:本对象都要没了,去重表再留着就是泄漏。
1809
+ this._res.stop(); // 资源管理器:全景/平面那条链停、显示闸开、显存账作废(内部同样落到 `stopGpuPrewarm`)
1719
1810
  this.stopGpuPrewarm();
1720
1811
  this._stitcher.dispose();
1721
1812
  this._glRenderer = null;
@@ -75,12 +75,19 @@ import { stripAssetHost, absAssetUrl } from "./assetUrl";
75
75
  // 原先各住在本站两个渲染器里一份 ⇒ 下游直接用包里的 `MktMesh` 时,存档里的 `images`/`models` 两键没人读。
76
76
  // 入库之后它们挂在 `MktMesh` **自己的子树**下,与墙同一棵变换 ⇒ 预览那三根 transform 滑杆搬得动整份展品。
77
77
  import { ImagePlane, type ImagePlaneData } from "./imagePlane";
78
- import { ModelSlot, type ModelSlotData } from "./modelSlot";
78
+ import { ModelSlot, glbFileName, type ModelSlotData } from "./modelSlot";
79
79
  import { ImagePlaneTileLoader } from "./imageTileLoader";
80
-
81
- /** 载入 JSON 时**优先**加载几个全景的素材(2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」)。
82
- * 口径 = 存档 `textures` 数组的前 n 项;其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。 */
83
- const PANO_IMPORT_PRIORITY = 2;
80
+ // ⭐ 用户需求 2026-11-05「专门写一个资源管理器来管理, 每个 mktmesh 都有自己的一个资源管理器来管理」:
81
+ // 隐藏期(`visible = false` ⇒ `onBeforeRender` 一次都不触发)里那三族资源的**驱动次序**与**是否已进显存**这本账。
82
+ import { MktResourceManager } from "./mktResManager";
83
+
84
+ /** 载入 JSON 时**优先**加载几个全景的素材的**默认档**(数值只写这一处)。
85
+ * 口径 = 存档 `textures` 数组的前 n 项(n = -1 视为整份全加载);其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。
86
+ * 2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」立的是 2;
87
+ * 2026-11-05 需求逐字「现在要默认把全景全部都要默认加载, 现在需要通过配置来设置, 提前加载多少个全景 -1 就是全加载 0就是不加载 1就是加载个 2就提前加载2个 一次类推, 默认加载前4个」⇒ 换成可配置的 4;
88
+ * 同日需求逐字「panoPriorityCount 设置为 -1, 不管有没有显示出来的全景图都应该默认全部加载, 等待渲染器的预载入」⇒ 默认档落 **-1(整份全加载)**。
89
+ * ⚠️ 这一档管的是**下载与拼合的次序**;「亮出来之前先把它推进显存」是 `preRender(renderer)` 那一手(见 `./mktResManager`)。 */
90
+ const PANO_IMPORT_PRIORITY_DEFAULT = -1;
84
91
 
85
92
  /** 隐藏期预热那一次绘制用的镜头(模块级共用:它只是个「有合法的投影矩阵可读」的载体,永不动 ⇒ ⛔ 不必每实例造一份)。 */
86
93
  const GPU_WARM_CAMERA = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
@@ -231,6 +238,17 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
231
238
  return this._panoTexMode;
232
239
  }
233
240
 
241
+ /** 优先加载的全景个数(存档 `textures` 的前 n 项):**-1 = 全部、0 = 一个都不提前、n = 前 n 个**(2026-11-05 需求)。
242
+ * 默认取 `PANO_IMPORT_PRIORITY_DEFAULT`(= -1,同日需求「不管有没有显示出来的全景图都应该默认全部加载」);
243
+ * 宿主想收窄,在渲染循环跑起来之前 `setPanoPriorityCount(4)` 一行即可
244
+ * (与 `_ossSize`、`_panoTexMode` 同一套注入路径 ⇒ 编辑器与预览各自的配置各自推下来)。
245
+ * ⚠️ 这一档只定**下载与拼合的次序**;隐藏期里谁来推那一拍 = `preRender(renderer)`(需求第 1、2 点)。 */
246
+ private _panoPriorityCount: number = PANO_IMPORT_PRIORITY_DEFAULT;
247
+ /** 当前优先加载个数(只读,回显用)。 */
248
+ get panoPriorityCount(): number {
249
+ return this._panoPriorityCount;
250
+ }
251
+
234
252
  // 复用临时对象(避免每帧 new,降低 GC 压力)
235
253
  private _tmpV3: Vector3 = new Vector3();
236
254
 
@@ -275,6 +293,26 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
275
293
  */
276
294
  private _revealPending: Array<ImagePlane | ModelSlot> = [];
277
295
 
296
+ /**
297
+ * ⭐ 2026-11-05 需求「专门写一个资源管理器来管理,每个 mktmesh 都有自己的一个资源管理器」:本对象一份。
298
+ * 它不下载、不解码、不上传 ⇒ 全部经下面那几只手点本对象上**现成**的入口(拼合器 `pump` / `prefetchHeadPanoramas` /
299
+ * `feedImagePlaneTiles` / 隐藏期预热),只多管两件事:隐藏期由谁来推那一拍、以及「哪一档真画进过显存」这本账。
300
+ */
301
+ private _res = new MktResourceManager({
302
+ prewarm: (renderer) => this.startGpuPrewarm(renderer),
303
+ stopPrewarm: () => this.stopGpuPrewarm(),
304
+ // 挂着牌 = 还没摘牌且还没亮出来 ⇒ 与 `_topUpGpuPrewarm` 用的同一颗尺(⛔ 不自立第二道闸)
305
+ prewarming: () => this._warmSeen !== null && !this.visible,
306
+ shown: () => this.visible,
307
+ pumpPano: (renderer) => this._stitcher.pump(renderer),
308
+ prefetchPano: () => this.prefetchHeadPanoramas(),
309
+ feedPlanes: () => this.feedImagePlaneTiles(),
310
+ });
311
+ /** 本对象的资源管理器(只读)。宿主只管调 `preRender`,需要看账本时才用它。 */
312
+ get resManager(): MktResourceManager {
313
+ return this._res;
314
+ }
315
+
278
316
  /**
279
317
  * 隐藏期 GPU 预热的在册清单(元素 = 一个「会进渲染列表的物件」,即它的材质组合 × 几何)。
280
318
  * null = 没在预热(未启动 / 已跑完 / `stopGpuPrewarm` / `dispose` 四种都是它)。
@@ -323,6 +361,8 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
323
361
  // `updateTexture`(它里面那记 `pump` 是「第一次拿到 renderer ⇒ 在册组整份作废」的现场)之后、
324
362
  // `feedFaceMapping`(名册逐槽入册)之前 —— 顺序倒过来就是「名册在前、我们垫底」= 静默失效。
325
363
  // 编辑态那份在 `MktDevMesh` 的宿主循环里照同一相对位置点(⛔ 别在这儿加第三处)。
364
+ // 2026-11-05 需求:这一批点几颗改由 `panoPriorityCount` 定(默认 -1 = 存档全部)⇒ 本句不动;
365
+ // 同一批在**隐藏期**由 `preRender(renderer)` 逐 tick 推(本钩子在那时一次都不触发)。
326
366
  this.prefetchHeadPanoramas();
327
367
  this.feedFaceMapping();
328
368
  this.tickProgressiveReveal();
@@ -1108,6 +1148,18 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1108
1148
  this._panoTexMode = next;
1109
1149
  }
1110
1150
 
1151
+ /** 设置优先加载的全景个数:-1 = 全部 / 0 = 不提前 / n = 存档前 n 个(2026-11-05 需求)。
1152
+ * 收口:负数(含 -2、NaN 一类)一律归到 -1「全加载」,正数向下取整;非有限值回落默认档。
1153
+ * 改档⛔ 需要重新 `importData` —— 名册每帧重排,下一帧 `prefetchHeadPanoramas` 直接用新数点名
1154
+ * (`prioritize` 幂等 ⇒ 已在队列里的那批不重复投喂,也不会把已出队的旧批再塞回去)。 */
1155
+ setPanoPriorityCount(count: number): void {
1156
+ if (!Number.isFinite(count)) this._panoPriorityCount = PANO_IMPORT_PRIORITY_DEFAULT;
1157
+ else {
1158
+ const n = Math.floor(count);
1159
+ this._panoPriorityCount = n < 0 ? -1 : n;
1160
+ }
1161
+ }
1162
+
1111
1163
  /**
1112
1164
  * 记下「当前激活(名册 0 号槽 = 中心)是第几个全景」。
1113
1165
  * ⭐ 皇上旨 2026-10-03 旨②:预览立方体已与墙**共用同一份材质**,原来那句「把中心那几张抄给立方体单全景材质」
@@ -1237,6 +1289,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1237
1289
  ms.dispose();
1238
1290
  }
1239
1291
  this.models = [];
1292
+ // 用户 2026-11-04「日志少打印点吧」⇒ 这一路(进厅 / 切项目 / 预览的整层重建)几十颗⛔ 不再一颗一行:
1293
+ // 名单先攒着,循环结束打**一条**(文件名口径与 `setModelUrl` 那一记共用 `glbFileName`)⇒ 一颗不落、一行到位。
1294
+ const glbNames: string[] = [];
1240
1295
  for (const d of datas) {
1241
1296
  const ms = new ModelSlot(d.name);
1242
1297
  ms.applyData(d);
@@ -1248,9 +1303,11 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1248
1303
  //(⚠️ 与旧的「GLB 在路上时这一槽露出占位方块」不同:整层重建这一路一颗都不露,直到轮到自己亮)
1249
1304
  ms.visible = false;
1250
1305
  this._revealPending.push(ms);
1306
+ glbNames.push(glbFileName(d.url));
1251
1307
  void ms.loadModel(d.url).then(() => this._queueModelMount(ms, onEachLoad));
1252
1308
  }
1253
1309
  }
1310
+ if (glbNames.length > 0) console.log(`[MktMesh] 整层重建加载 ${glbNames.length} 个 glb:${glbNames.join("、")}`);
1254
1311
  }
1255
1312
 
1256
1313
  /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
@@ -1343,12 +1400,15 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1343
1400
  * 表空了它就是一句 `length` 判断,⛔ 不需要再像旧写法那样把钩子置 null(钩子上还挂着全景投影那三件事)。
1344
1401
  * ⚠️ 这里写的 `visible` 只针对**自己压进表的那几颗** ⇒ 表外的对象(单颗新增、已 dispose、被 `releaseReveal`
1345
1402
  * 摘牌的)一律不碰;要在放行前接管某颗,调 `releaseReveal(o)`。
1403
+ * ⭐ 2026-11-05 需求第 4 点「只已经预载入的资源才显示出来」:内容落定(`o.loaded`)之后还要问一句资源管理器
1404
+ * 「这一档画进显存了吗」—— 只有宿主调过 `preRender` 且预热还挂着牌时才拦,摘牌 / 没调过 ⇒ 一律放行(⛔ 不许拦出永久黑屏)。
1346
1405
  */
1347
1406
  tickProgressiveReveal(): void {
1348
1407
  const q = this._revealPending;
1349
1408
  for (let i = 0; i < q.length; i++) {
1350
1409
  const o = q[i];
1351
1410
  if (!o.loaded) continue; // 内容还在路上 ⇒ 这一颗继续压着,先看后面已经落定的
1411
+ if (this._res.holdsReveal(o)) continue; // 内容到了但还没进显存 ⇒ 等 `preRender` 那一拍画过它
1352
1412
  q.splice(i, 1);
1353
1413
  o.visible = true;
1354
1414
  return;
@@ -1357,6 +1417,26 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1357
1417
 
1358
1418
  // ─────────────────── 隐藏期 GPU 预热(2026-11-01 需求:进厅那一下不要再现编着色器) ───────────────────
1359
1419
 
1420
+ /**
1421
+ * ⭐ 2026-11-05 需求第 1 点逐字:「需要提供一个 `preRender` 方法, 传入一个渲染器对象, 然后在这个方法里面使用
1422
+ * 渲染来预载入已经准备好的资源到 GPU」。隐藏期(`visible = false`)里 three 遍历第一句就把本对象连同子树跳过
1423
+ * ⇒ `onBeforeRender` 一次都不触发,全景瓦片不下载不拼合、平面不绑、GLB 与墙的 program 不编不传;
1424
+ * 本方法就是那句「藏起来的时候替它跑一遍渲染循环里的推进」,交进来的渲染器同时供拼合器与预热用。
1425
+ *
1426
+ * 一次点齐三族(次序固定,与 `onBeforeRender` 里同一相对位置):
1427
+ * ① 全景 —— `stitcher.pump(renderer)` 逐 tick 走「补下载 → 画已进显存的格 → 传一张新的」,
1428
+ * 额度由 `panoPriorityCount` 点的批排在前(默认 -1 = 整份存档,需求第 2 点);
1429
+ * ② 平面图 —— `feedImagePlaneTiles()` 一次把整层地址交给下载、绑定一帧一颗(需求第 3 点:这两族本来就全量发起);
1430
+ * ③ GLB / 平面 / 墙 —— 下面的 `startGpuPrewarm` 那一族逐 tick 画一件进 1×1 目标(program 编译 + 顶点缓冲首传)。
1431
+ *
1432
+ * 幂等 ⇒ 隐藏期里每帧点一次、或只在载入后点一次都行;已排进队列的那条链不会被重复起。
1433
+ * 本对象一旦 `visible`(进厅),链自己停 ⇒ 之后交回 `onBeforeRender` 每帧那一路,⛔ 不抢同一帧的额度。
1434
+ * ⛔ 不调本方法的宿主(发布页直接亮墙、编辑器画布)行为与改造前逐字一致:显示闸只在 `preRender` 点过之后才生效。
1435
+ */
1436
+ preRender(renderer: WebGLRenderer): void {
1437
+ this._res.preRender(renderer);
1438
+ }
1439
+
1360
1440
  /**
1361
1441
  * 开始隐藏期预热:把「进厅第一帧要现编的那些 program + 要第一次上传的那些几何/纹理」提前跑掉,一帧只放**一件**。
1362
1442
  *
@@ -1453,6 +1533,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1453
1533
  if (seen.has(key)) return;
1454
1534
  seen.add(key);
1455
1535
  items.push({ src: o, m: mesh.material as Material | Material[], g });
1536
+ // ⭐ 2026-11-05 需求第 4 点:把「这一颗属于哪一档」交给资源管理器记账(键的算式仍只在这里这一处),
1537
+ // 等 `_runGpuWarmStep` 真画过它再翻成「已进显存」⇒ `tickProgressiveReveal` 才放行。
1538
+ this._res.noteWarmKey(o, key);
1456
1539
  added++;
1457
1540
  });
1458
1541
  return added;
@@ -1502,6 +1585,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1502
1585
  renderer.setRenderTarget(this._warmRt!);
1503
1586
  renderer.render(scene, GPU_WARM_CAMERA);
1504
1587
  renderer.setRenderTarget(prevTarget);
1588
+ // ⭐ 2026-11-05 需求第 4 点:这一档刚刚真画进显存(program 编了、顶点缓冲与纹理首传也做了)⇒ 记上「已常驻」,
1589
+ // `tickProgressiveReveal` 那一拍才许把对应那颗亮出来(同一档的第二、第三颗跟着一起放行)。
1590
+ this._res.markWarmDrawn(it.src);
1505
1591
  this._scheduleGpuWarmStep(renderer);
1506
1592
  }
1507
1593
 
@@ -1683,11 +1769,12 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1683
1769
  }
1684
1770
 
1685
1771
  /**
1686
- * 把**存档里前 `PANO_IMPORT_PRIORITY` 个全景**按存档顺序登记进拼合器的**加载任务队列**
1772
+ * 把**存档里前 `panoPriorityCount` 个全景**(-1 = 整份全加载)按存档顺序登记进拼合器的**加载任务队列**
1687
1773
  *(2026-10-29 需求「优先加载前 2 个全景素材」+ 2026-10-30 需求逐字:「给 mktmesh 内部加一个加载任务队列,
1688
1774
  * 把模型和平面图排在最前面,然后按照顺序,依次把前 2 个全景的顺序把瓦片加载任务添加到任务队列里面,
1689
1775
  * 第一个加载完了,就把这个加载任务从任务队列里面去掉,然后加载第二个;
1690
- * 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」)。
1776
+ * 如果后续再按照距离计算需要加载哪个全景的时候,同样地添加到加载任务队列的后面等待加载」
1777
+ * + 2026-11-05 需求「提前加载多少个全景改由配置决定,-1 全加载 / 0 不加载 / n 前 n 个,默认 4」)。
1691
1778
  * 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
1692
1779
  *(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
1693
1780
  *
@@ -1696,27 +1783,31 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1696
1783
  * (GLB 逐只 `loadModel` 不 throttle;平面那 4 片一次性 `_imgTileLoader.preload`),
1697
1784
  * 而全景瓦片最早也要等下一帧的 `pump` 才发第一片 ⇒ 发起次序天然是「模型 → 平面图 → 全景」,
1698
1785
  * ⛔ 这里不再把它们塞进队列重复管一遍(塞了就是第二把尺)。
1699
- * ② 本方法把存档 0 号、1 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
1786
+ * ② 本方法把存档 0 号起这 n 号依次 `prioritize` 进队列 ⇒ 拼合器每帧**只喂队首那一组**,
1700
1787
  * 0 号 32 格全落定(画进 RT,或该格本就缺地址/判死)才出队、1 号才开始发第一片。
1701
1788
  * 比 10-29 那版(只是把两批挪到在册顺序前面、额度仍被两批分摊)更硬:**第 1 颗独享额度,最快出全图**。
1789
+ * n = -1(全加载)时这一条变成「整份存档按存档顺序串行」⇒ 距离名册插不到前面,出清速度取决于总片数。
1702
1790
  * ③ 名册按距离点到的其他全景走 `feedFaceMapping` 里那句 `ensure(t.tiles)`(不带队列)⇒ 只入册、
1703
1791
  * 排在队列后面等,队列空了才按在册顺序补料(现行口径不变)。
1704
1792
  * ⚠️⚠️ 为什么每帧点而⛔ 只在 `importData` 里点一记:预览每开一次都换渲染器,`_switchRenderer` 把在册组
1705
1793
  * 整份作废(队列里的键也在同一刻失效);只在载入那次点名等于白点。本方法点在 `pump` 之后、名册逐槽
1706
- * `ensure` 之前 ⇒ 每帧把这两记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以 0 号恒在 1 号前)。
1794
+ * `ensure` 之前 ⇒ 每帧把这几记任务补回队列头部(`prioritize` 幂等,已在队列里就不挪,所以存档次序恒不变)。
1707
1795
  * ⚠️ 代价如实记:
1708
- * · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前两颗的那面墙会多灰一会儿。
1796
+ * · 队列非空时**其他全景一片都不发** ⇒ 开机那几秒里,离注视点最近、但不在存档前 n 颗的那面墙会多灰一会儿。
1709
1797
  * · 一颗「永远下不完」的瓦片(下载失败还没累计到判死)会压住队列;判死阈值是解码失败 3 次
1710
- * (`_dead`),退避重试期间队首不落定 ⇒ 第 2 颗跟着延后。真实数据里缺片是「地址为空/404」,
1798
+ * (`_dead`),退避重试期间队首不落定 ⇒ 后面几颗跟着延后。真实数据里缺片是「地址为空/404」,
1711
1799
  * 404 走三次即判死、那一格拼合涂灰 ⇒ 队列照样出清。
1712
- * · 这两颗全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
1713
- * (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这两个景点迟早要建。
1800
+ * · 这批全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
1801
+ * (现行口径每颗全景 2 槽 × 6144² ≈ 384MB 显存)—— 组在运行中不淘汰,走进这些景点迟早要建;
1802
+ * ⚠️ n = -1(全加载)时这一条按存档全景总数放大,显存与在途请求都跟着涨。
1714
1803
  * ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
1715
1804
  * 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
1716
1805
  * 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
1717
1806
  */
1718
1807
  prefetchHeadPanoramas(): void {
1719
- const n = Math.min(PANO_IMPORT_PRIORITY, this.textures.length);
1808
+ const total = this.textures.length;
1809
+ // -1 = 整份;0 = 一个都不提前(这一句直接空转,其余全景仍按距离名册排队 = 回到 10-29 之前的行为)
1810
+ const n = this._panoPriorityCount < 0 ? total : Math.min(this._panoPriorityCount, total);
1720
1811
  // 正着点:0 号先入队 ⇒ 队首是它,落定后才轮到 1 号(`prioritize` 对已在队列里的键不挪位置)
1721
1812
  for (let i = 0; i < n; i++) {
1722
1813
  const info = this.textures[i];
@@ -1757,6 +1848,7 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1757
1848
  this._revealPending = [];
1758
1849
  // 隐藏期预热:清单、去重表与小屋子那套临时资源随本对象一起退场(那张 1×1 渲染目标是本类**唯一**自建的 RT)。
1759
1850
  // 走 `stopGpuPrewarm` 而不是 `_finishGpuPrewarm`:本对象都要没了,去重表再留着就是泄漏。
1851
+ this._res.stop(); // 资源管理器:全景/平面那条链停、显示闸开、显存账作废(内部同样落到 `stopGpuPrewarm`)
1760
1852
  this.stopGpuPrewarm();
1761
1853
  this._stitcher.dispose();
1762
1854
  this._glRenderer = null;