three-base 2.0.190 → 2.0.193

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.
@@ -4,6 +4,7 @@ import { type PanoStitcher } from "./panoTileStitcher";
4
4
  import type { MktMeshData, MktTextureInfo, WallPoint, WallSide } from "./types";
5
5
  import { ImagePlane, type ImagePlaneData } from "./imagePlane";
6
6
  import { ModelSlot, type ModelSlotData } from "./modelSlot";
7
+ import { GlassPane, type GlassPaneData } from "./glassPane";
7
8
  export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
8
9
  /** 纹理列表(每个纹理的 url + 世界矩阵) */
9
10
  textures: MktTextureInfo[];
@@ -128,6 +129,23 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
128
129
  imagePlanes: ImagePlane[];
129
130
  /** 模型槽层(同一棵子树,口径同上) */
130
131
  models: ModelSlot[];
132
+ /**
133
+ * 玻璃面层(⭐ 2026-10-28 需求「正方的面」):同一棵子树,口径与上面两层一致。
134
+ * 与那两层的区别只在材质(MeshPhysicalMaterial + transmission + DoubleSide,见 `./glassPane.ts`)
135
+ * ⇒ 它**没有自己的内容纹理**,反射源共用下面那一张 `_glassEnvTex`。
136
+ */
137
+ glassPanes: GlassPane[];
138
+ /**
139
+ * 玻璃面反射环境的**源地址** = 第一个全景的整图地址(`textures[0].url`),空串 = 还没有全景可用。
140
+ * ⚠️ 这里存的是**未拼尺寸参数的原始地址**(与 `_texCache` 的键同一口径)⇒ 全景换了地址才发现要重载;
141
+ * 下载时按当前 `_ossSize` 档过 `resolveOssUrl`(默认为 1024 那一档)。PMREM 卷积输出的分辨率上限远小于 1024
142
+ * ⇒ 反射精度由那一步决定,用缩放副本不降低观感,换来的是省掉整张原图(实测 11904×5952 ≈ 283MB)的解码内存。
143
+ * ⚠️ 为什么不用「第一个全景那两张拼合槽 RT」:每张 RT 装的是原图**左/右半幅**且压成 1:1 方图,
144
+ * 当等距柱状环境采会横向拉伸一倍 + 缺半边;且它的 mapping 是 UVMapping,three 不会替它做 PMREM ⇒ 采出花屏。
145
+ */
146
+ private _glassEnvKey;
147
+ /** 已就绪的反射环境纹理(null = 未就绪 / 没有全景);同一张共用给所有玻璃面 ⇒ 只下载、只卷积一次 */
148
+ private _glassEnvTex;
131
149
  /**
132
150
  * 那 4 张瓦片的下载/解码缓存。⭐ 与全景 `_texCache` 同一条旨(10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」)
133
151
  * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不会再空一格;只随本对象 `dispose()` 释放。
@@ -147,6 +165,20 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
147
165
  * 一帧只有一颗进 `_contentRoot`(进图那一拍要重算包围盒 + 排布 + 首次把几何送上 GPU)。
148
166
  */
149
167
  private _modelMountQueued;
168
+ /**
169
+ * 「压着不亮、等内容落定后一帧放一颗」的在册表(2026-10-28 需求:进展厅时平面图与 GLB 全在同几帧里
170
+ * 首次送 GPU —— 顶点缓冲上传 + program 编译都在「亮出来的那一帧」,几十颗挤一起就是那一下卡)。
171
+ *
172
+ * ⚠️ 为什么闸口在**本对象的 `onBeforeRender`**、不在这颗平面/模型自己的:three 遍历场景图的第一句就是
173
+ * `if ( object.visible === false ) return;`(`node_modules/three/build/three.module.js:29837`),
174
+ * 它和整棵子树当帧直接跳过 ⇒ 隐形对象**一次都不会**收到 `onBeforeRender`,挂在自己身上 = 永远不亮。
175
+ * 墙(本对象)每帧都进渲染列表,所以闸口设在这座上,在册表里逐帧放行。
176
+ * ⚠️ 单颗新增那两路(编辑态双击落点 / 复制 / 上传)⛔ 不进这张表 —— 刚加的东西必须当场看得见。
177
+ * ⚠️ `visible` 本是业务的旋钮,这里只是**借一帧**:整层重建那两路的对象是当场新建的(默认可见),所以进表前
178
+ * 不需要再看它一眼;但**放行那一拍之前**要接管某颗的显隐,必须调 `releaseReveal(o)` 摘牌,
179
+ * 否则它会在轮到自身那一帧被写回可见(`boolean` 只有一个位,放行那里分不出这笔是谁写的)。
180
+ */
181
+ private _revealPending;
150
182
  constructor(opts?: {
151
183
  wallHeight?: number;
152
184
  wallClosed?: boolean;
@@ -327,10 +359,17 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
327
359
  addImagePlane(ip: ImagePlane): ImagePlane;
328
360
  /** 摘掉一颗并释放它自己的几何/材质;那 4 张瓦片纹理归 `_imgTileLoader` 管 ⇒ ⛔ 不在这里 dispose */
329
361
  removeImagePlane(ip: ImagePlane): void;
330
- /** 单颗模型槽入层(编辑态双击落点、以及「复制模型」用) */
362
+ /** 单颗模型槽入层(编辑态双击落点、以及「复制模型」用):⛔ 不压点亮 —— 刚加的必须当场看得见 */
331
363
  addModel(ms: ModelSlot): ModelSlot;
332
364
  /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾;排在后头的挂载帧靠「不在册」早退) */
333
365
  removeModel(ms: ModelSlot): void;
366
+ /** 出「逐帧点亮」那张在册表(删除 / 整层重建旧的一批都走这里,⛔ 不许留已经 dispose 的对象) */
367
+ private _dropReveal;
368
+ /**
369
+ * 把某颗平面/模型的显隐控制权交回调用方(2026-10-28:`visible` 上有业务在用,点亮那一拍⛔ 不能覆盖
370
+ * 放行之前写下的那一笔)。摘牌后它**仍是压着的**(整层重建那一路进来就是 `false`),要它可见就自己写 `true`。
371
+ */
372
+ releaseReveal(o: ImagePlane | ModelSlot): void;
334
373
  /**
335
374
  * 整层重建图片平面(`importData` 与预览那一路)。
336
375
  * ⚠️ 进来的地址必须是**已补回域名**的绝对形态:补域名只有 `importData` 那一处(与 `textures` 同一把 `assetUrl` 尺),
@@ -344,12 +383,41 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
344
383
  * 2026-10-26 决定:取回与「挂进父元素」分两拍 —— 地址一次性全交给下载(网络 + 解析不占渲染帧),
345
384
  * 真正进场景图那一步交给 `threeUtils.addWork` ⇒ 全局 rAF 每拍只跑一项 = **一帧只挂一颗槽**
346
385
  * (挂载那一拍要重算包围盒 + 排布 + 首次把整棵子树的几何送上 GPU,十几颗挤在同一帧就是他要拆掉的那一卡)。
386
+ * 2026-10-28 再加第三拍「点亮」:挂载那一拍走的是 setTimeout 链,主线程被几十次 GLB 解析压住时渲染帧会滞后,
387
+ * 于是好几颗在同一记 render 里首次上 GPU ⇒ 整层重建出来的槽先压着不亮,由 `tickProgressiveReveal`
388
+ * **一帧只放一颗**(那一下顶点缓冲上传 + program 编译才真正摊到 N 帧上)。单颗那两路(落点新增 / 复制 / 上传)不受这一拍管。
347
389
  */
348
390
  setModels(datas: ModelSlotData[], onEachLoad?: () => void): void;
349
391
  /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
350
392
  private _queueModelMount;
351
393
  /** 全局队列里的那一帧:只挂这一槽,挂好才回调(`onEachLoad` = 按需渲染宿主的标脏钩子) */
352
394
  private _mountModelStep;
395
+ /** 单颗玻璃面入层(编辑态双击落点用):进数组 + 挂进本对象子树 + 先吃当前反射源,再确认源在下路没有 */
396
+ addGlassPane(gp: GlassPane): GlassPane;
397
+ /** 摘掉一颗并释放它的几何/材质;反射纹理是共用那张 ⇒ 最后一颗也摘掉时才由 `syncGlassEnvironment` 收(按源地址记账,⛔ 不在这里直接 dispose) */
398
+ removeGlassPane(gp: GlassPane): void;
399
+ /**
400
+ * 整层重建玻璃面(`importData` 与预览那一路)。玻璃面不引用任何素材文件 ⇒ ⛔ 不过 `absAssetUrl`
401
+ * (地址只有反射源那一处,而它取的是**本对象内存里**的 `textures[0].url`,已经是补过域名的形态)。
402
+ */
403
+ setGlassPanes(datas: GlassPaneData[]): void;
404
+ /**
405
+ * 把「第一个全景」的整图当成玻璃面的反射环境(2026-10-28 需求)。
406
+ *
407
+ * 判据 = 源地址变了没有(⛔ 不按下标记:删掉 0 号景点后原 1 号自动顶上来,那时地址会变 ⇒ 自愈)。
408
+ * 调用点只有四处,⛔ 不进 `onBeforeRender` 自驱:① 单颗入层 ② 单颗摘除 ③ 整层重建 ④ 编辑态每帧 tick 里与
409
+ * `feedImagePlaneTiles()` 并排那一句(源地址没变就一句比较直接返回 ⇒ 无玻璃面的场景零成本,
410
+ * 而全景被增删/换图那些路都不必各自记得再来点一次)。
411
+ *
412
+ * ⚠️ 换源那一步必须**先摘后放**:材质还指着这张、位图却已 `close()` ⇒ 采出来是空图。
413
+ * ⚠️ 下载走 `loadPanoTexture`(解码在工作线程)并自动向 `DefaultLoadingManager` 记账 ⇒ 预览弹窗那个
414
+ * 加载进度条能看见它,与全景瓦片同一本账。
415
+ * ⚠️ 结果只存进 `_glassEnvTex`,⛔ 不进 `_texCache`:那张表在 `setOssSize` 换档时会被整批 dispose
416
+ * (`_reloadActiveTexture`),而正在被材质引用的这张不能被那样顺手清掉。
417
+ */
418
+ syncGlassEnvironment(): void;
419
+ /** 把同一张反射源发给每一颗玻璃面(`GlassPane` 内部按对象判等 ⇒ 重复发同一张不会重编译)。 */
420
+ private _applyGlassEnv;
353
421
  /**
354
422
  * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
355
423
  * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
@@ -365,6 +433,15 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
365
433
  private _queueImagePlaneBind;
366
434
  /** 队列里的一格:只绑**这一颗**平面里已就绪的那几格;还有在途的就重新排队,⛔ 不在这里等、也⛔ 不顺手把别的平面一起做完。 */
367
435
  private _bindImagePlaneStep;
436
+ /**
437
+ * 逐帧点亮:每帧从在册表里放行**一颗**(内容已落定的第一颗),亮完就把这一颗出册。
438
+ * ⚠️ 一记只放一颗 ⇒ 顶点缓冲上传与首次 program 编译被摊到 N 帧,而不是同几帧里全上(那一下卡)。
439
+ * ⚠️ 由 `onBeforeRender` 每帧调(编辑态那份在 `MktDevMesh`,覆写时⛔ 不许漏掉这一句);
440
+ * 表空了它就是一句 `length` 判断,⛔ 不需要再像旧写法那样把钩子置 null(钩子上还挂着全景投影那三件事)。
441
+ * ⚠️ 这里写的 `visible` 只针对**自己压进表的那几颗** ⇒ 表外的对象(单颗新增、已 dispose、被 `releaseReveal`
442
+ * 摘牌的)一律不碰;要在放行前接管某颗,调 `releaseReveal(o)`。
443
+ */
444
+ tickProgressiveReveal(): void;
368
445
  /** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
369
446
  * @param projName 项目名(可选,优先于 `setProjectName` 注入的那个)——图片地址落成 `{项目名}/xxx.jpg` */
370
447
  exportData(projName?: string): MktMeshData;
@@ -372,6 +449,37 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
372
449
  * @param projName 项目名(可选)——用于把落盘的 `{项目名}/x_0.jpg` 还原回真身OSS 地址
373
450
  * @param onModelLoad 每只 GLB 落地时回调一次(可选)——按需渲染的宿主(编辑器)拿它标脏;每帧真画的预览不传 */
374
451
  importData(data: MktMeshData, projName?: string, onModelLoad?: () => void): void;
375
- /** 释放 GPU 资源:瓦片缓存(**只有这里**清,皇上旨 2026-10-02)、拼合器、平面图/模型两层、几何、材质 */
452
+ /**
453
+ * 把**存档里前 `PANO_IMPORT_PRIORITY` 个全景**的素材点名抢在别的之前加载(2026-10-29 需求)。
454
+ * 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
455
+ *(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
456
+ *
457
+ * 为什么能「先加载」:拼合器每帧那三道工序(补下载 → 画已进 GPU 的 → 再传一张)都按
458
+ * `_groups` 这张 Map 的**在册顺序**取料,而下载额度只有「每帧 2 张 / 在途 ≤8 / 已解码待上传 ≤8」
459
+ * (`panoTileStitcher.ts` 顶部那三个常数)⇒ 排在头部的两组把额度吃满,其余全景要等它们画满才轮得到。
460
+ * 平时这个顺序由每帧的槽位名册定(离注视点近的先下),本方法把它钉成「存档数组的前两个」,与机位无关。
461
+ * ⚠️⚠️ 为什么点名时要带第三个参 `front = true`(每帧把这两批**挪回队首**)而⛔ 不只是「先入册」:
462
+ * 在册顺序是拼合器自己的状态 —— 后入册的只会**追加到队尾**,而 `ensure` 幂等、已在册就不再移动。
463
+ * 预览这条路恰好是最坏的那种排法:`feedFaceMapping` 开头还有一记 `pump`,拼合器**第一次**拿到渲染器时
464
+ * 必走 `_switchRenderer`(`null → 真 renderer` 也算换,弹窗每开一次就是换一次)把在册组整份作废,
465
+ * 随后名册按距离重新入册占了队首 ⇒ 本方法下一帧重新入册只能排在它们后面,而且**从此再也翻不了身**。
466
+ * 2026-10-29 逐帧打点就是这个形状:帧 1 `p0,p1,p11…p4` → 帧 2 起 `p11…p4,p0,p1`,
467
+ * 40 帧下来 p0 只下到 1 片、p1 下到 30 片,离注视点最近那颗第 13 条请求就开始了 = 线上报的现象。
468
+ * ⇒ 带 `front` 之后顺序每帧重新成立,与谁先入册、作废过几次、机位在哪都无关
469
+ * (真跑自证在护栏 BE ④:`D` 后入册排队尾,带 `front` 再点一次 ⇒ 立刻回到队首、下一帧下载全归它)。
470
+ * ⚠️ 为什么是**每帧**而不是「载入那一次点一记」:上面那次作废就发生在载入之后的第一帧,
471
+ * 只在 `importData` 里点名等于白点;本方法点在 `pump` 之后、名册逐槽 `ensure` 之前,且每帧都点
472
+ * (已在册且已在队首 ⇒ `_toFront` 里一次 `keys().next()` 就返回,没有额外开销)。
473
+ * ⛔ 不另立第二把下载尺:这里用的就是 `feedFaceMapping` 每帧在用的同一个 `ensure(info.tiles, …)`
474
+ * 与同一个批次键(第一条非空瓦片地址)⇒ 后面那句自然判重,一片瓦片终身只 `fetch` 一次。
475
+ * ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
476
+ * 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
477
+ * 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
478
+ * ⚠️ 代价如实记:这两颗全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
479
+ * (现行口径每颗全景 2 槽 × 6144² ≈ 400MB 显存)—— 组在运行中不淘汰,走进那两个景点迟早要建,
480
+ * 这里只是把这一笔挪到了载入的第一帧。
481
+ */
482
+ prefetchHeadPanoramas(): void;
483
+ /** 释放 GPU 资源:瓦片缓存(**只有这里**清,2026-10-02 决定)、拼合器、平面图 / 模型 / 玻璃面三层、几何、材质 */
376
484
  dispose(): this;
377
485
  }
@@ -23,7 +23,7 @@
23
23
  * 说明:所有实例共享同一份 material(单一 panorama uniform),
24
24
  * 同一时刻只激活名册 0 号槽(中心)那一个纹理(全局共享单激活方案)。
25
25
  */
26
- import { BufferGeometry, BackSide, Float32BufferAttribute, Matrix4, Mesh, Shape, ShapeGeometry, SRGBColorSpace, Vector3, Vector4, } from "three";
26
+ import { BufferGeometry, BackSide, EquirectangularReflectionMapping, Float32BufferAttribute, Matrix4, Mesh, Shape, ShapeGeometry, SRGBColorSpace, Vector3, Vector4, } from "three";
27
27
  import { createPlaceholderTexture, isPanoTexMode, mktPanoTiledFlag, mktPanoTexUrls, isRtStitchKey, rtStitchKeyParts, RT_STITCH_KEY_PREFIX, RT_STITCH_SLOT_COUNT } from "./mktShader";
28
28
  import { MKT_PANO_MAX_SLOTS } from "./mktMappingShader";
29
29
  import { createMktFloorMappingShaderMaterial } from "./mktMappingShader";
@@ -51,6 +51,12 @@ import { stripAssetHost, absAssetUrl } from "./assetUrl";
51
51
  import { ImagePlane } from "./imagePlane";
52
52
  import { ModelSlot } from "./modelSlot";
53
53
  import { ImagePlaneTileLoader } from "./imageTileLoader";
54
+ // ⭐ 2026-10-28 需求「正方的面 + 双面 + 反射用第一个全景 + 进 json 回显」:第三层,形状与上面两层同口径
55
+ // (挂在本网格子树、随展品一起走;数据由自己的 `toData/applyData` 逐字存)。
56
+ import { GlassPane } from "./glassPane";
57
+ /** 载入 JSON 时**优先**加载几个全景的素材(2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」)。
58
+ * 口径 = 存档 `textures` 数组的前 n 项;其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。 */
59
+ const PANO_IMPORT_PRIORITY = 2;
54
60
  export default class MktMesh extends Mesh {
55
61
  /** 纹理列表(每个纹理的 url + 世界矩阵) */
56
62
  textures = [];
@@ -185,6 +191,23 @@ export default class MktMesh extends Mesh {
185
191
  imagePlanes = [];
186
192
  /** 模型槽层(同一棵子树,口径同上) */
187
193
  models = [];
194
+ /**
195
+ * 玻璃面层(⭐ 2026-10-28 需求「正方的面」):同一棵子树,口径与上面两层一致。
196
+ * 与那两层的区别只在材质(MeshPhysicalMaterial + transmission + DoubleSide,见 `./glassPane.ts`)
197
+ * ⇒ 它**没有自己的内容纹理**,反射源共用下面那一张 `_glassEnvTex`。
198
+ */
199
+ glassPanes = [];
200
+ /**
201
+ * 玻璃面反射环境的**源地址** = 第一个全景的整图地址(`textures[0].url`),空串 = 还没有全景可用。
202
+ * ⚠️ 这里存的是**未拼尺寸参数的原始地址**(与 `_texCache` 的键同一口径)⇒ 全景换了地址才发现要重载;
203
+ * 下载时按当前 `_ossSize` 档过 `resolveOssUrl`(默认为 1024 那一档)。PMREM 卷积输出的分辨率上限远小于 1024
204
+ * ⇒ 反射精度由那一步决定,用缩放副本不降低观感,换来的是省掉整张原图(实测 11904×5952 ≈ 283MB)的解码内存。
205
+ * ⚠️ 为什么不用「第一个全景那两张拼合槽 RT」:每张 RT 装的是原图**左/右半幅**且压成 1:1 方图,
206
+ * 当等距柱状环境采会横向拉伸一倍 + 缺半边;且它的 mapping 是 UVMapping,three 不会替它做 PMREM ⇒ 采出花屏。
207
+ */
208
+ _glassEnvKey = "";
209
+ /** 已就绪的反射环境纹理(null = 未就绪 / 没有全景);同一张共用给所有玻璃面 ⇒ 只下载、只卷积一次 */
210
+ _glassEnvTex = null;
188
211
  /**
189
212
  * 那 4 张瓦片的下载/解码缓存。⭐ 与全景 `_texCache` 同一条旨(10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」)
190
213
  * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不会再空一格;只随本对象 `dispose()` 释放。
@@ -204,6 +227,20 @@ export default class MktMesh extends Mesh {
204
227
  * 一帧只有一颗进 `_contentRoot`(进图那一拍要重算包围盒 + 排布 + 首次把几何送上 GPU)。
205
228
  */
206
229
  _modelMountQueued = new Set();
230
+ /**
231
+ * 「压着不亮、等内容落定后一帧放一颗」的在册表(2026-10-28 需求:进展厅时平面图与 GLB 全在同几帧里
232
+ * 首次送 GPU —— 顶点缓冲上传 + program 编译都在「亮出来的那一帧」,几十颗挤一起就是那一下卡)。
233
+ *
234
+ * ⚠️ 为什么闸口在**本对象的 `onBeforeRender`**、不在这颗平面/模型自己的:three 遍历场景图的第一句就是
235
+ * `if ( object.visible === false ) return;`(`node_modules/three/build/three.module.js:29837`),
236
+ * 它和整棵子树当帧直接跳过 ⇒ 隐形对象**一次都不会**收到 `onBeforeRender`,挂在自己身上 = 永远不亮。
237
+ * 墙(本对象)每帧都进渲染列表,所以闸口设在这座上,在册表里逐帧放行。
238
+ * ⚠️ 单颗新增那两路(编辑态双击落点 / 复制 / 上传)⛔ 不进这张表 —— 刚加的东西必须当场看得见。
239
+ * ⚠️ `visible` 本是业务的旋钮,这里只是**借一帧**:整层重建那两路的对象是当场新建的(默认可见),所以进表前
240
+ * 不需要再看它一眼;但**放行那一拍之前**要接管某颗的显隐,必须调 `releaseReveal(o)` 摘牌,
241
+ * 否则它会在轮到自身那一帧被写回可见(`boolean` 只有一个位,放行那里分不出这笔是谁写的)。
242
+ */
243
+ _revealPending = [];
207
244
  constructor(opts = {}) {
208
245
  // 发布/预览态墙:**固定**用「全景序号直取」着色器(皇上旨 2026-09-29:mktmesh 就只有按照 attribute
209
246
  // 配置来显示全景这一种固定的 shader——每帧由 feedFaceMapping 按 panoFaceMapping 把被引用的全景
@@ -233,7 +270,13 @@ export default class MktMesh extends Mesh {
233
270
  this._glRenderer = renderer;
234
271
  this.syncProjInvMatrix();
235
272
  this.updateTexture(camera);
273
+ // ⭐ 2026-10-29 需求「载入 json 优先加载前 2 个全景图的素材,然后再加载其他的」:必须点在
274
+ // `updateTexture`(它里面那记 `pump` 是「第一次拿到 renderer ⇒ 在册组整份作废」的现场)之后、
275
+ // `feedFaceMapping`(名册逐槽入册)之前 —— 顺序倒过来就是「名册在前、我们垫底」= 静默失效。
276
+ // 编辑态那份在 `MktDevMesh` 的宿主循环里照同一相对位置点(⛔ 别在这儿加第三处)。
277
+ this.prefetchHeadPanoramas();
236
278
  this.feedFaceMapping();
279
+ this.tickProgressiveReveal();
237
280
  };
238
281
  }
239
282
  // ───────────────────────── 墙体几何(由点位挤出) ─────────────────────────
@@ -1055,10 +1098,11 @@ export default class MktMesh extends Mesh {
1055
1098
  this.imagePlanes.splice(i, 1);
1056
1099
  this._imgBindQueued.delete(ip);
1057
1100
  this._imgBindDone.delete(ip);
1101
+ this._dropReveal(ip);
1058
1102
  this.remove(ip);
1059
1103
  ip.dispose();
1060
1104
  }
1061
- /** 单颗模型槽入层(编辑态双击落点、以及「复制模型」用) */
1105
+ /** 单颗模型槽入层(编辑态双击落点、以及「复制模型」用):⛔ 不压点亮 —— 刚加的必须当场看得见 */
1062
1106
  addModel(ms) {
1063
1107
  this.models.push(ms);
1064
1108
  this.add(ms);
@@ -1070,9 +1114,23 @@ export default class MktMesh extends Mesh {
1070
1114
  if (i >= 0)
1071
1115
  this.models.splice(i, 1);
1072
1116
  this._modelMountQueued.delete(ms);
1117
+ this._dropReveal(ms);
1073
1118
  this.remove(ms);
1074
1119
  ms.dispose();
1075
1120
  }
1121
+ /** 出「逐帧点亮」那张在册表(删除 / 整层重建旧的一批都走这里,⛔ 不许留已经 dispose 的对象) */
1122
+ _dropReveal(o) {
1123
+ const i = this._revealPending.indexOf(o);
1124
+ if (i >= 0)
1125
+ this._revealPending.splice(i, 1);
1126
+ }
1127
+ /**
1128
+ * 把某颗平面/模型的显隐控制权交回调用方(2026-10-28:`visible` 上有业务在用,点亮那一拍⛔ 不能覆盖
1129
+ * 放行之前写下的那一笔)。摘牌后它**仍是压着的**(整层重建那一路进来就是 `false`),要它可见就自己写 `true`。
1130
+ */
1131
+ releaseReveal(o) {
1132
+ this._dropReveal(o);
1133
+ }
1076
1134
  /**
1077
1135
  * 整层重建图片平面(`importData` 与预览那一路)。
1078
1136
  * ⚠️ 进来的地址必须是**已补回域名**的绝对形态:补域名只有 `importData` 那一处(与 `textures` 同一把 `assetUrl` 尺),
@@ -1082,6 +1140,7 @@ export default class MktMesh extends Mesh {
1082
1140
  this._imgBindQueued.clear();
1083
1141
  this._imgBindDone.clear();
1084
1142
  for (const ip of this.imagePlanes) {
1143
+ this._dropReveal(ip);
1085
1144
  this.remove(ip);
1086
1145
  ip.dispose();
1087
1146
  }
@@ -1091,6 +1150,12 @@ export default class MktMesh extends Mesh {
1091
1150
  ip.applyData(d);
1092
1151
  this.imagePlanes.push(ip);
1093
1152
  this.add(ip);
1153
+ // 压着等点亮:4 格地址齐全才有东西可画(片数那把尺 = 下面 `_queueImagePlaneBind` 的 `tiles.length !== 4`,
1154
+ // ⛔ 不是这里另立一份);不齐的那一档本来就整片 discard、永远落不定 ⇒ 不压(省得白占一帧在册表)。
1155
+ if (d.tiles && d.tiles.length === 4) {
1156
+ ip.visible = false;
1157
+ this._revealPending.push(ip);
1158
+ }
1094
1159
  }
1095
1160
  // 整层重建完立刻起排:地址一次性全交给下载、绑材质逐帧一记 ⇒ 宿主不驱动这一层也长得出来
1096
1161
  this.feedImagePlaneTiles();
@@ -1102,10 +1167,14 @@ export default class MktMesh extends Mesh {
1102
1167
  * 2026-10-26 决定:取回与「挂进父元素」分两拍 —— 地址一次性全交给下载(网络 + 解析不占渲染帧),
1103
1168
  * 真正进场景图那一步交给 `threeUtils.addWork` ⇒ 全局 rAF 每拍只跑一项 = **一帧只挂一颗槽**
1104
1169
  * (挂载那一拍要重算包围盒 + 排布 + 首次把整棵子树的几何送上 GPU,十几颗挤在同一帧就是他要拆掉的那一卡)。
1170
+ * 2026-10-28 再加第三拍「点亮」:挂载那一拍走的是 setTimeout 链,主线程被几十次 GLB 解析压住时渲染帧会滞后,
1171
+ * 于是好几颗在同一记 render 里首次上 GPU ⇒ 整层重建出来的槽先压着不亮,由 `tickProgressiveReveal`
1172
+ * **一帧只放一颗**(那一下顶点缓冲上传 + program 编译才真正摊到 N 帧上)。单颗那两路(落点新增 / 复制 / 上传)不受这一拍管。
1105
1173
  */
1106
1174
  setModels(datas, onEachLoad) {
1107
1175
  this._modelMountQueued.clear();
1108
1176
  for (const ms of this.models) {
1177
+ this._dropReveal(ms);
1109
1178
  this.remove(ms);
1110
1179
  ms.dispose();
1111
1180
  }
@@ -1114,9 +1183,15 @@ export default class MktMesh extends Mesh {
1114
1183
  const ms = new ModelSlot(d.name);
1115
1184
  ms.applyData(d);
1116
1185
  this.models.push(ms);
1117
- this.add(ms); // 槽本体(占位小立方体)当场进图 ⇒ GLB 在路上时这一槽不凭空消失
1118
- if (d.url)
1186
+ // 槽本体(占位小立方体)当场进图:在册表、射线拾取、`exportData` 都立刻认它
1187
+ this.add(ms);
1188
+ if (d.url) {
1189
+ // 压着等点亮:GLB 落进 `_contentRoot` 那一拍之后,还要再等一次「一帧一颗」的放行才上 GPU
1190
+ //(⚠️ 与旧的「GLB 在路上时这一槽露出占位方块」不同:整层重建这一路一颗都不露,直到轮到自己亮)
1191
+ ms.visible = false;
1192
+ this._revealPending.push(ms);
1119
1193
  void ms.loadModel(d.url).then(() => this._queueModelMount(ms, onEachLoad));
1194
+ }
1120
1195
  }
1121
1196
  }
1122
1197
  /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
@@ -1136,6 +1211,91 @@ export default class MktMesh extends Mesh {
1136
1211
  if (ms.mountStaged() && onEachLoad)
1137
1212
  onEachLoad();
1138
1213
  }
1214
+ // ───────────── 玻璃面层(⭐ 2026-10-28 需求「正方的面:双面 + 反射用第一个全景 + 进 json 回显」) ─────────────
1215
+ /** 单颗玻璃面入层(编辑态双击落点用):进数组 + 挂进本对象子树 + 先吃当前反射源,再确认源在下路没有 */
1216
+ addGlassPane(gp) {
1217
+ this.glassPanes.push(gp);
1218
+ this.add(gp);
1219
+ gp.setEnvironmentTexture(this._glassEnvTex);
1220
+ this.syncGlassEnvironment();
1221
+ return gp;
1222
+ }
1223
+ /** 摘掉一颗并释放它的几何/材质;反射纹理是共用那张 ⇒ 最后一颗也摘掉时才由 `syncGlassEnvironment` 收(按源地址记账,⛔ 不在这里直接 dispose) */
1224
+ removeGlassPane(gp) {
1225
+ const i = this.glassPanes.indexOf(gp);
1226
+ if (i >= 0)
1227
+ this.glassPanes.splice(i, 1);
1228
+ this.remove(gp);
1229
+ gp.dispose();
1230
+ this.syncGlassEnvironment();
1231
+ }
1232
+ /**
1233
+ * 整层重建玻璃面(`importData` 与预览那一路)。玻璃面不引用任何素材文件 ⇒ ⛔ 不过 `absAssetUrl`
1234
+ * (地址只有反射源那一处,而它取的是**本对象内存里**的 `textures[0].url`,已经是补过域名的形态)。
1235
+ */
1236
+ setGlassPanes(datas) {
1237
+ for (const gp of this.glassPanes) {
1238
+ this.remove(gp);
1239
+ gp.dispose();
1240
+ }
1241
+ this.glassPanes = [];
1242
+ for (const d of datas) {
1243
+ const gp = new GlassPane(d.name);
1244
+ gp.applyData(d);
1245
+ this.glassPanes.push(gp);
1246
+ this.add(gp);
1247
+ }
1248
+ this.syncGlassEnvironment();
1249
+ }
1250
+ /**
1251
+ * 把「第一个全景」的整图当成玻璃面的反射环境(2026-10-28 需求)。
1252
+ *
1253
+ * 判据 = 源地址变了没有(⛔ 不按下标记:删掉 0 号景点后原 1 号自动顶上来,那时地址会变 ⇒ 自愈)。
1254
+ * 调用点只有四处,⛔ 不进 `onBeforeRender` 自驱:① 单颗入层 ② 单颗摘除 ③ 整层重建 ④ 编辑态每帧 tick 里与
1255
+ * `feedImagePlaneTiles()` 并排那一句(源地址没变就一句比较直接返回 ⇒ 无玻璃面的场景零成本,
1256
+ * 而全景被增删/换图那些路都不必各自记得再来点一次)。
1257
+ *
1258
+ * ⚠️ 换源那一步必须**先摘后放**:材质还指着这张、位图却已 `close()` ⇒ 采出来是空图。
1259
+ * ⚠️ 下载走 `loadPanoTexture`(解码在工作线程)并自动向 `DefaultLoadingManager` 记账 ⇒ 预览弹窗那个
1260
+ * 加载进度条能看见它,与全景瓦片同一本账。
1261
+ * ⚠️ 结果只存进 `_glassEnvTex`,⛔ 不进 `_texCache`:那张表在 `setOssSize` 换档时会被整批 dispose
1262
+ * (`_reloadActiveTexture`),而正在被材质引用的这张不能被那样顺手清掉。
1263
+ */
1264
+ syncGlassEnvironment() {
1265
+ // 没有玻璃面 ⇒ 源地址按「空」算:下面那一段照常走换源流程,把共用那张位图关干净(⛔ 不让它悬着)
1266
+ const key = this.glassPanes.length === 0 ? "" : (this.textures[0]?.url ?? "");
1267
+ if (key === this._glassEnvKey)
1268
+ return;
1269
+ this._glassEnvKey = key;
1270
+ if (this._glassEnvTex) {
1271
+ this._applyGlassEnv(null);
1272
+ releasePanoTexture(this._glassEnvTex);
1273
+ this._glassEnvTex = null;
1274
+ }
1275
+ if (!key)
1276
+ return;
1277
+ loadPanoTexture(this.resolveOssUrl(key, false)).then((tex) => {
1278
+ // 这一张到货时源可能已被后来的换源(切项目 / 换全景)顶掉 ⇒ 就地释放,⛔ 不许写回材质
1279
+ if (this._glassEnvKey !== key) {
1280
+ releasePanoTexture(tex);
1281
+ return;
1282
+ }
1283
+ tex.colorSpace = SRGBColorSpace;
1284
+ // three 的 `WebGLCubeUVMaps` 只对这个标记才自动 PMREM 卷积(示例也是把等距柱状图直接挂 material.envMap)
1285
+ tex.mapping = EquirectangularReflectionMapping;
1286
+ this._glassEnvTex = tex;
1287
+ this._applyGlassEnv(tex);
1288
+ }, () => {
1289
+ // 单次失败:清掉记账 ⇒ 下一次有人调本方法(新增/重建玻璃面、编辑态 tick)自然重试,
1290
+ // 与全景瓦片那条「失败后下一帧同键再试」同口径,只是这里由调用点驱动而非每帧驱动。
1291
+ this._glassEnvKey = "";
1292
+ });
1293
+ }
1294
+ /** 把同一张反射源发给每一颗玻璃面(`GlassPane` 内部按对象判等 ⇒ 重复发同一张不会重编译)。 */
1295
+ _applyGlassEnv(tex) {
1296
+ for (const gp of this.glassPanes)
1297
+ gp.setEnvironmentTexture(tex);
1298
+ }
1139
1299
  /**
1140
1300
  * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
1141
1301
  * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
@@ -1202,6 +1362,25 @@ export default class MktMesh extends Mesh {
1202
1362
  }
1203
1363
  this._imgBindDone.set(ip, sig); // 全部落定(含下载失败那格:它留 null,那一格就永久 discard)
1204
1364
  }
1365
+ /**
1366
+ * 逐帧点亮:每帧从在册表里放行**一颗**(内容已落定的第一颗),亮完就把这一颗出册。
1367
+ * ⚠️ 一记只放一颗 ⇒ 顶点缓冲上传与首次 program 编译被摊到 N 帧,而不是同几帧里全上(那一下卡)。
1368
+ * ⚠️ 由 `onBeforeRender` 每帧调(编辑态那份在 `MktDevMesh`,覆写时⛔ 不许漏掉这一句);
1369
+ * 表空了它就是一句 `length` 判断,⛔ 不需要再像旧写法那样把钩子置 null(钩子上还挂着全景投影那三件事)。
1370
+ * ⚠️ 这里写的 `visible` 只针对**自己压进表的那几颗** ⇒ 表外的对象(单颗新增、已 dispose、被 `releaseReveal`
1371
+ * 摘牌的)一律不碰;要在放行前接管某颗,调 `releaseReveal(o)`。
1372
+ */
1373
+ tickProgressiveReveal() {
1374
+ const q = this._revealPending;
1375
+ for (let i = 0; i < q.length; i++) {
1376
+ const o = q[i];
1377
+ if (!o.loaded)
1378
+ continue; // 内容还在路上 ⇒ 这一颗继续压着,先看后面已经落定的
1379
+ q.splice(i, 1);
1380
+ o.visible = true;
1381
+ return;
1382
+ }
1383
+ }
1205
1384
  /** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
1206
1385
  * @param projName 项目名(可选,优先于 `setProjectName` 注入的那个)——图片地址落成 `{项目名}/xxx.jpg` */
1207
1386
  exportData(projName) {
@@ -1238,6 +1417,9 @@ export default class MktMesh extends Mesh {
1238
1417
  const d = ms.toData();
1239
1418
  return { ...d, url: d.url ? stripAssetHost(d.url, pn) : "" };
1240
1419
  }),
1420
+ // 玻璃面层(2026-10-28 需求):只有位置/旋转/边长,⛔ 不过 `stripAssetHost` —— 它不引用任何素材,
1421
+ // 反射源是运行时从 `textures[0].url` 取的(上面那个键自己就落进存档了)。
1422
+ glass: this.glassPanes.map((gp) => gp.toData()),
1241
1423
  };
1242
1424
  }
1243
1425
  /** 导入数据:重建墙体几何与纹理列表,并激活第一个纹理
@@ -1293,9 +1475,57 @@ export default class MktMesh extends Mesh {
1293
1475
  tiles: (d.tiles ?? []).map((u) => absAssetUrl(u, pn)),
1294
1476
  })));
1295
1477
  this.setModels((data.models ?? []).map((d) => ({ ...d, url: d.url ? absAssetUrl(d.url, pn) : "" })), onModelLoad);
1478
+ // 玻璃面层(2026-10-28 需求):排在 setTextures **之后**是硬要求 —— 它的反射源读的就是刚还原好的
1479
+ // `textures[0].url`(补过域名的形态),排在前面会用上一次项目的地址起一次错的载。
1480
+ // 旧存档没有这个键 ⇒ 空数组 = 整层清空,与上面两层的口径逐字一致。
1481
+ this.setGlassPanes(data.glass ?? []);
1482
+ }
1483
+ /**
1484
+ * 把**存档里前 `PANO_IMPORT_PRIORITY` 个全景**的素材点名抢在别的之前加载(2026-10-29 需求)。
1485
+ * 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
1486
+ *(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
1487
+ *
1488
+ * 为什么能「先加载」:拼合器每帧那三道工序(补下载 → 画已进 GPU 的 → 再传一张)都按
1489
+ * `_groups` 这张 Map 的**在册顺序**取料,而下载额度只有「每帧 2 张 / 在途 ≤8 / 已解码待上传 ≤8」
1490
+ * (`panoTileStitcher.ts` 顶部那三个常数)⇒ 排在头部的两组把额度吃满,其余全景要等它们画满才轮得到。
1491
+ * 平时这个顺序由每帧的槽位名册定(离注视点近的先下),本方法把它钉成「存档数组的前两个」,与机位无关。
1492
+ * ⚠️⚠️ 为什么点名时要带第三个参 `front = true`(每帧把这两批**挪回队首**)而⛔ 不只是「先入册」:
1493
+ * 在册顺序是拼合器自己的状态 —— 后入册的只会**追加到队尾**,而 `ensure` 幂等、已在册就不再移动。
1494
+ * 预览这条路恰好是最坏的那种排法:`feedFaceMapping` 开头还有一记 `pump`,拼合器**第一次**拿到渲染器时
1495
+ * 必走 `_switchRenderer`(`null → 真 renderer` 也算换,弹窗每开一次就是换一次)把在册组整份作废,
1496
+ * 随后名册按距离重新入册占了队首 ⇒ 本方法下一帧重新入册只能排在它们后面,而且**从此再也翻不了身**。
1497
+ * 2026-10-29 逐帧打点就是这个形状:帧 1 `p0,p1,p11…p4` → 帧 2 起 `p11…p4,p0,p1`,
1498
+ * 40 帧下来 p0 只下到 1 片、p1 下到 30 片,离注视点最近那颗第 13 条请求就开始了 = 线上报的现象。
1499
+ * ⇒ 带 `front` 之后顺序每帧重新成立,与谁先入册、作废过几次、机位在哪都无关
1500
+ * (真跑自证在护栏 BE ④:`D` 后入册排队尾,带 `front` 再点一次 ⇒ 立刻回到队首、下一帧下载全归它)。
1501
+ * ⚠️ 为什么是**每帧**而不是「载入那一次点一记」:上面那次作废就发生在载入之后的第一帧,
1502
+ * 只在 `importData` 里点名等于白点;本方法点在 `pump` 之后、名册逐槽 `ensure` 之前,且每帧都点
1503
+ * (已在册且已在队首 ⇒ `_toFront` 里一次 `keys().next()` 就返回,没有额外开销)。
1504
+ * ⛔ 不另立第二把下载尺:这里用的就是 `feedFaceMapping` 每帧在用的同一个 `ensure(info.tiles, …)`
1505
+ * 与同一个批次键(第一条非空瓦片地址)⇒ 后面那句自然判重,一片瓦片终身只 `fetch` 一次。
1506
+ * ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
1507
+ * 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
1508
+ * 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
1509
+ * ⚠️ 代价如实记:这两颗全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
1510
+ * (现行口径每颗全景 2 槽 × 6144² ≈ 400MB 显存)—— 组在运行中不淘汰,走进那两个景点迟早要建,
1511
+ * 这里只是把这一笔挪到了载入的第一帧。
1512
+ */
1513
+ prefetchHeadPanoramas() {
1514
+ const n = Math.min(PANO_IMPORT_PRIORITY, this.textures.length);
1515
+ // ⚠️ **倒着**点:`front` 是「挪到最前」,正着点会让 0 号被 1 号压到后面(实测队首变成 `p1,p0`)
1516
+ // ⇒ 倒着点,最后一手挪的是 0 号,存档序号小的仍排在更前。
1517
+ for (let i = n - 1; i >= 0; i--) {
1518
+ const info = this.textures[i];
1519
+ // 与 `feedFaceMapping` 里那一句同判据:真瓦片才进拼合器,整图形态进瓦片缓存
1520
+ // 第三个参 `true` = 把这批**挪到在册顺序最前**(⛔ 光靠「先入册」押不住顺序,见方法头)
1521
+ if (mktPanoTiledFlag(info.tiles, this._panoTexMode) === 1)
1522
+ this._stitcher.ensure(info.tiles, undefined, true);
1523
+ else if (info.url)
1524
+ this.ensureTileTexture(info.url, false);
1525
+ }
1296
1526
  }
1297
1527
  // ───────────────────────── 释放 ─────────────────────────
1298
- /** 释放 GPU 资源:瓦片缓存(**只有这里**清,皇上旨 2026-10-02)、拼合器、平面图/模型两层、几何、材质 */
1528
+ /** 释放 GPU 资源:瓦片缓存(**只有这里**清,2026-10-02 决定)、拼合器、平面图 / 模型 / 玻璃面三层、几何、材质 */
1299
1529
  dispose() {
1300
1530
  // ⚠️ rt 虚拟键从不进 `_texCache`(拼合槽纹理由 `_stitcher` 自己持有并释放),这里清的都是本类下载的 Texture。
1301
1531
  // ⭐ 甲(2026-10-05):`releasePanoTexture` 除 `dispose()` 外还要 `ImageBitmap.close()` —— 位图的原生内存
@@ -1318,10 +1548,21 @@ export default class MktMesh extends Mesh {
1318
1548
  ms.dispose();
1319
1549
  }
1320
1550
  this.models = [];
1551
+ // 玻璃面层:几何/材质各归各颗释放;那张共用的反射纹理在这里连位图一起关
1552
+ // (⛔ 它从不在 `_texCache` 里 ⇒ 上面那一轮清不到它,漏了这一句就是每次换页漏一张全景整图)。
1553
+ for (const gp of this.glassPanes) {
1554
+ this.remove(gp);
1555
+ gp.dispose();
1556
+ }
1557
+ this.glassPanes = [];
1558
+ releasePanoTexture(this._glassEnvTex);
1559
+ this._glassEnvTex = null;
1560
+ this._glassEnvKey = "";
1321
1561
  this._imgTileLoader.clear();
1322
1562
  this._imgBindQueued.clear();
1323
1563
  this._imgBindDone.clear();
1324
1564
  this._modelMountQueued.clear();
1565
+ this._revealPending = [];
1325
1566
  this._stitcher.dispose();
1326
1567
  this._glRenderer = null;
1327
1568
  if (this._placeholder)