three-base 2.0.192 → 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.
package/EventMap.d.ts CHANGED
@@ -8,6 +8,12 @@ export interface LoadingEvent {
8
8
  export interface OnLoadEvent {
9
9
  type: "onLoad";
10
10
  }
11
+ export interface LoaderProgressEvent {
12
+ type: "progress";
13
+ url: string;
14
+ loaded: number;
15
+ total: number;
16
+ }
11
17
  export interface BeforeRenderEvent {
12
18
  type: "beforeRender";
13
19
  }
@@ -53,6 +59,7 @@ export interface MeshBaseEventMap extends Object3DEventMap {
53
59
  export interface ThreeLoaderEventMap extends Object3DEventMap {
54
60
  "loadding": LoadingEvent;
55
61
  "onLoad": OnLoadEvent;
62
+ "progress": LoaderProgressEvent;
56
63
  }
57
64
  export interface ThreeBaseEventTypeMap extends Object3DEventMap {
58
65
  "loadding": LoadingEvent;
package/EventMap.ts CHANGED
@@ -29,6 +29,13 @@ import type { Object3DEventMap, Vector3 } from "three";
29
29
  export interface LoadedEvent { type: "loaded"; }
30
30
  export interface LoadingEvent { type: "loadding"; }
31
31
  export interface OnLoadEvent { type: "onLoad"; }
32
+ // 资源加载进度:`DefaultLoadingManager.onProgress` 的三个数原样带出(url / 已完成项数 / 总项数)
33
+ export interface LoaderProgressEvent {
34
+ type: "progress";
35
+ url: string;
36
+ loaded: number;
37
+ total: number;
38
+ }
32
39
  export interface BeforeRenderEvent { type: "beforeRender"; }
33
40
  export interface AfterRenderEvent { type: "afterRender"; }
34
41
 
@@ -77,6 +84,7 @@ export interface MeshBaseEventMap extends Object3DEventMap {
77
84
  export interface ThreeLoaderEventMap extends Object3DEventMap {
78
85
  "loadding": LoadingEvent;
79
86
  "onLoad": OnLoadEvent;
87
+ "progress": LoaderProgressEvent;
80
88
  }
81
89
 
82
90
  export interface ThreeBaseEventTypeMap extends Object3DEventMap {
@@ -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()` 释放。
@@ -374,6 +392,32 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
374
392
  private _queueModelMount;
375
393
  /** 全局队列里的那一帧:只挂这一槽,挂好才回调(`onEachLoad` = 按需渲染宿主的标脏钩子) */
376
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;
377
421
  /**
378
422
  * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
379
423
  * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
@@ -405,6 +449,37 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
405
449
  * @param projName 项目名(可选)——用于把落盘的 `{项目名}/x_0.jpg` 还原回真身OSS 地址
406
450
  * @param onModelLoad 每只 GLB 落地时回调一次(可选)——按需渲染的宿主(编辑器)拿它标脏;每帧真画的预览不传 */
407
451
  importData(data: MktMeshData, projName?: string, onModelLoad?: () => void): void;
408
- /** 释放 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 决定)、拼合器、平面图 / 模型 / 玻璃面三层、几何、材质 */
409
484
  dispose(): this;
410
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()` 释放。
@@ -247,6 +270,11 @@ export default class MktMesh extends Mesh {
247
270
  this._glRenderer = renderer;
248
271
  this.syncProjInvMatrix();
249
272
  this.updateTexture(camera);
273
+ // ⭐ 2026-10-29 需求「载入 json 优先加载前 2 个全景图的素材,然后再加载其他的」:必须点在
274
+ // `updateTexture`(它里面那记 `pump` 是「第一次拿到 renderer ⇒ 在册组整份作废」的现场)之后、
275
+ // `feedFaceMapping`(名册逐槽入册)之前 —— 顺序倒过来就是「名册在前、我们垫底」= 静默失效。
276
+ // 编辑态那份在 `MktDevMesh` 的宿主循环里照同一相对位置点(⛔ 别在这儿加第三处)。
277
+ this.prefetchHeadPanoramas();
250
278
  this.feedFaceMapping();
251
279
  this.tickProgressiveReveal();
252
280
  };
@@ -1183,6 +1211,91 @@ export default class MktMesh extends Mesh {
1183
1211
  if (ms.mountStaged() && onEachLoad)
1184
1212
  onEachLoad();
1185
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
+ }
1186
1299
  /**
1187
1300
  * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
1188
1301
  * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
@@ -1304,6 +1417,9 @@ export default class MktMesh extends Mesh {
1304
1417
  const d = ms.toData();
1305
1418
  return { ...d, url: d.url ? stripAssetHost(d.url, pn) : "" };
1306
1419
  }),
1420
+ // 玻璃面层(2026-10-28 需求):只有位置/旋转/边长,⛔ 不过 `stripAssetHost` —— 它不引用任何素材,
1421
+ // 反射源是运行时从 `textures[0].url` 取的(上面那个键自己就落进存档了)。
1422
+ glass: this.glassPanes.map((gp) => gp.toData()),
1307
1423
  };
1308
1424
  }
1309
1425
  /** 导入数据:重建墙体几何与纹理列表,并激活第一个纹理
@@ -1359,9 +1475,57 @@ export default class MktMesh extends Mesh {
1359
1475
  tiles: (d.tiles ?? []).map((u) => absAssetUrl(u, pn)),
1360
1476
  })));
1361
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
+ }
1362
1526
  }
1363
1527
  // ───────────────────────── 释放 ─────────────────────────
1364
- /** 释放 GPU 资源:瓦片缓存(**只有这里**清,皇上旨 2026-10-02)、拼合器、平面图/模型两层、几何、材质 */
1528
+ /** 释放 GPU 资源:瓦片缓存(**只有这里**清,2026-10-02 决定)、拼合器、平面图 / 模型 / 玻璃面三层、几何、材质 */
1365
1529
  dispose() {
1366
1530
  // ⚠️ rt 虚拟键从不进 `_texCache`(拼合槽纹理由 `_stitcher` 自己持有并释放),这里清的都是本类下载的 Texture。
1367
1531
  // ⭐ 甲(2026-10-05):`releasePanoTexture` 除 `dispose()` 外还要 `ImageBitmap.close()` —— 位图的原生内存
@@ -1384,6 +1548,16 @@ export default class MktMesh extends Mesh {
1384
1548
  ms.dispose();
1385
1549
  }
1386
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 = "";
1387
1561
  this._imgTileLoader.clear();
1388
1562
  this._imgBindQueued.clear();
1389
1563
  this._imgBindDone.clear();
@@ -28,6 +28,7 @@ import {
28
28
  BufferGeometry,
29
29
  Camera,
30
30
  BackSide,
31
+ EquirectangularReflectionMapping,
31
32
  Float32BufferAttribute,
32
33
  Material,
33
34
  Matrix4,
@@ -71,6 +72,13 @@ import { stripAssetHost, absAssetUrl } from "./assetUrl";
71
72
  import { ImagePlane, type ImagePlaneData } from "./imagePlane";
72
73
  import { ModelSlot, type ModelSlotData } from "./modelSlot";
73
74
  import { ImagePlaneTileLoader } from "./imageTileLoader";
75
+ // ⭐ 2026-10-28 需求「正方的面 + 双面 + 反射用第一个全景 + 进 json 回显」:第三层,形状与上面两层同口径
76
+ // (挂在本网格子树、随展品一起走;数据由自己的 `toData/applyData` 逐字存)。
77
+ import { GlassPane, type GlassPaneData } from "./glassPane";
78
+
79
+ /** 载入 JSON 时**优先**加载几个全景的素材(2026-10-29 需求「优先加载前 2 个全景图的素材,然后再加载其他的」)。
80
+ * 口径 = 存档 `textures` 数组的前 n 项;其余全景照常按每帧的槽位名册(离注视点最近 8 个)排队,只是排在它们后面。 */
81
+ const PANO_IMPORT_PRIORITY = 2;
74
82
 
75
83
  export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
76
84
  /** 纹理列表(每个纹理的 url + 世界矩阵) */
@@ -211,6 +219,23 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
211
219
  imagePlanes: ImagePlane[] = [];
212
220
  /** 模型槽层(同一棵子树,口径同上) */
213
221
  models: ModelSlot[] = [];
222
+ /**
223
+ * 玻璃面层(⭐ 2026-10-28 需求「正方的面」):同一棵子树,口径与上面两层一致。
224
+ * 与那两层的区别只在材质(MeshPhysicalMaterial + transmission + DoubleSide,见 `./glassPane.ts`)
225
+ * ⇒ 它**没有自己的内容纹理**,反射源共用下面那一张 `_glassEnvTex`。
226
+ */
227
+ glassPanes: GlassPane[] = [];
228
+ /**
229
+ * 玻璃面反射环境的**源地址** = 第一个全景的整图地址(`textures[0].url`),空串 = 还没有全景可用。
230
+ * ⚠️ 这里存的是**未拼尺寸参数的原始地址**(与 `_texCache` 的键同一口径)⇒ 全景换了地址才发现要重载;
231
+ * 下载时按当前 `_ossSize` 档过 `resolveOssUrl`(默认为 1024 那一档)。PMREM 卷积输出的分辨率上限远小于 1024
232
+ * ⇒ 反射精度由那一步决定,用缩放副本不降低观感,换来的是省掉整张原图(实测 11904×5952 ≈ 283MB)的解码内存。
233
+ * ⚠️ 为什么不用「第一个全景那两张拼合槽 RT」:每张 RT 装的是原图**左/右半幅**且压成 1:1 方图,
234
+ * 当等距柱状环境采会横向拉伸一倍 + 缺半边;且它的 mapping 是 UVMapping,three 不会替它做 PMREM ⇒ 采出花屏。
235
+ */
236
+ private _glassEnvKey: string = "";
237
+ /** 已就绪的反射环境纹理(null = 未就绪 / 没有全景);同一张共用给所有玻璃面 ⇒ 只下载、只卷积一次 */
238
+ private _glassEnvTex: Texture | null = null;
214
239
  /**
215
240
  * 那 4 张瓦片的下载/解码缓存。⭐ 与全景 `_texCache` 同一条旨(10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」)
216
241
  * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不会再空一格;只随本对象 `dispose()` 释放。
@@ -272,6 +297,11 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
272
297
  this._glRenderer = renderer;
273
298
  this.syncProjInvMatrix();
274
299
  this.updateTexture(camera as Camera);
300
+ // ⭐ 2026-10-29 需求「载入 json 优先加载前 2 个全景图的素材,然后再加载其他的」:必须点在
301
+ // `updateTexture`(它里面那记 `pump` 是「第一次拿到 renderer ⇒ 在册组整份作废」的现场)之后、
302
+ // `feedFaceMapping`(名册逐槽入册)之前 —— 顺序倒过来就是「名册在前、我们垫底」= 静默失效。
303
+ // 编辑态那份在 `MktDevMesh` 的宿主循环里照同一相对位置点(⛔ 别在这儿加第三处)。
304
+ this.prefetchHeadPanoramas();
275
305
  this.feedFaceMapping();
276
306
  this.tickProgressiveReveal();
277
307
  };
@@ -1217,6 +1247,96 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1217
1247
  if (ms.mountStaged() && onEachLoad) onEachLoad();
1218
1248
  }
1219
1249
 
1250
+ // ───────────── 玻璃面层(⭐ 2026-10-28 需求「正方的面:双面 + 反射用第一个全景 + 进 json 回显」) ─────────────
1251
+
1252
+ /** 单颗玻璃面入层(编辑态双击落点用):进数组 + 挂进本对象子树 + 先吃当前反射源,再确认源在下路没有 */
1253
+ addGlassPane(gp: GlassPane): GlassPane {
1254
+ this.glassPanes.push(gp);
1255
+ this.add(gp);
1256
+ gp.setEnvironmentTexture(this._glassEnvTex);
1257
+ this.syncGlassEnvironment();
1258
+ return gp;
1259
+ }
1260
+
1261
+ /** 摘掉一颗并释放它的几何/材质;反射纹理是共用那张 ⇒ 最后一颗也摘掉时才由 `syncGlassEnvironment` 收(按源地址记账,⛔ 不在这里直接 dispose) */
1262
+ removeGlassPane(gp: GlassPane): void {
1263
+ const i = this.glassPanes.indexOf(gp);
1264
+ if (i >= 0) this.glassPanes.splice(i, 1);
1265
+ this.remove(gp);
1266
+ gp.dispose();
1267
+ this.syncGlassEnvironment();
1268
+ }
1269
+
1270
+ /**
1271
+ * 整层重建玻璃面(`importData` 与预览那一路)。玻璃面不引用任何素材文件 ⇒ ⛔ 不过 `absAssetUrl`
1272
+ * (地址只有反射源那一处,而它取的是**本对象内存里**的 `textures[0].url`,已经是补过域名的形态)。
1273
+ */
1274
+ setGlassPanes(datas: GlassPaneData[]): void {
1275
+ for (const gp of this.glassPanes) {
1276
+ this.remove(gp);
1277
+ gp.dispose();
1278
+ }
1279
+ this.glassPanes = [];
1280
+ for (const d of datas) {
1281
+ const gp = new GlassPane(d.name);
1282
+ gp.applyData(d);
1283
+ this.glassPanes.push(gp);
1284
+ this.add(gp);
1285
+ }
1286
+ this.syncGlassEnvironment();
1287
+ }
1288
+
1289
+ /**
1290
+ * 把「第一个全景」的整图当成玻璃面的反射环境(2026-10-28 需求)。
1291
+ *
1292
+ * 判据 = 源地址变了没有(⛔ 不按下标记:删掉 0 号景点后原 1 号自动顶上来,那时地址会变 ⇒ 自愈)。
1293
+ * 调用点只有四处,⛔ 不进 `onBeforeRender` 自驱:① 单颗入层 ② 单颗摘除 ③ 整层重建 ④ 编辑态每帧 tick 里与
1294
+ * `feedImagePlaneTiles()` 并排那一句(源地址没变就一句比较直接返回 ⇒ 无玻璃面的场景零成本,
1295
+ * 而全景被增删/换图那些路都不必各自记得再来点一次)。
1296
+ *
1297
+ * ⚠️ 换源那一步必须**先摘后放**:材质还指着这张、位图却已 `close()` ⇒ 采出来是空图。
1298
+ * ⚠️ 下载走 `loadPanoTexture`(解码在工作线程)并自动向 `DefaultLoadingManager` 记账 ⇒ 预览弹窗那个
1299
+ * 加载进度条能看见它,与全景瓦片同一本账。
1300
+ * ⚠️ 结果只存进 `_glassEnvTex`,⛔ 不进 `_texCache`:那张表在 `setOssSize` 换档时会被整批 dispose
1301
+ * (`_reloadActiveTexture`),而正在被材质引用的这张不能被那样顺手清掉。
1302
+ */
1303
+ syncGlassEnvironment(): void {
1304
+ // 没有玻璃面 ⇒ 源地址按「空」算:下面那一段照常走换源流程,把共用那张位图关干净(⛔ 不让它悬着)
1305
+ const key = this.glassPanes.length === 0 ? "" : (this.textures[0]?.url ?? "");
1306
+ if (key === this._glassEnvKey) return;
1307
+ this._glassEnvKey = key;
1308
+ if (this._glassEnvTex) {
1309
+ this._applyGlassEnv(null);
1310
+ releasePanoTexture(this._glassEnvTex);
1311
+ this._glassEnvTex = null;
1312
+ }
1313
+ if (!key) return;
1314
+ loadPanoTexture(this.resolveOssUrl(key, false)).then(
1315
+ (tex) => {
1316
+ // 这一张到货时源可能已被后来的换源(切项目 / 换全景)顶掉 ⇒ 就地释放,⛔ 不许写回材质
1317
+ if (this._glassEnvKey !== key) {
1318
+ releasePanoTexture(tex);
1319
+ return;
1320
+ }
1321
+ tex.colorSpace = SRGBColorSpace;
1322
+ // three 的 `WebGLCubeUVMaps` 只对这个标记才自动 PMREM 卷积(示例也是把等距柱状图直接挂 material.envMap)
1323
+ tex.mapping = EquirectangularReflectionMapping;
1324
+ this._glassEnvTex = tex;
1325
+ this._applyGlassEnv(tex);
1326
+ },
1327
+ () => {
1328
+ // 单次失败:清掉记账 ⇒ 下一次有人调本方法(新增/重建玻璃面、编辑态 tick)自然重试,
1329
+ // 与全景瓦片那条「失败后下一帧同键再试」同口径,只是这里由调用点驱动而非每帧驱动。
1330
+ this._glassEnvKey = "";
1331
+ }
1332
+ );
1333
+ }
1334
+
1335
+ /** 把同一张反射源发给每一颗玻璃面(`GlassPane` 内部按对象判等 ⇒ 重复发同一张不会重编译)。 */
1336
+ private _applyGlassEnv(tex: Texture | null): void {
1337
+ for (const gp of this.glassPanes) gp.setEnvironmentTexture(tex);
1338
+ }
1339
+
1220
1340
  /**
1221
1341
  * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
1222
1342
  * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
@@ -1334,6 +1454,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1334
1454
  const d = ms.toData();
1335
1455
  return { ...d, url: d.url ? stripAssetHost(d.url, pn) : "" };
1336
1456
  }),
1457
+ // 玻璃面层(2026-10-28 需求):只有位置/旋转/边长,⛔ 不过 `stripAssetHost` —— 它不引用任何素材,
1458
+ // 反射源是运行时从 `textures[0].url` 取的(上面那个键自己就落进存档了)。
1459
+ glass: this.glassPanes.map((gp) => gp.toData()),
1337
1460
  };
1338
1461
  }
1339
1462
 
@@ -1396,11 +1519,58 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1396
1519
  (data.models ?? []).map((d) => ({ ...d, url: d.url ? absAssetUrl(d.url, pn) : "" })),
1397
1520
  onModelLoad
1398
1521
  );
1522
+ // 玻璃面层(2026-10-28 需求):排在 setTextures **之后**是硬要求 —— 它的反射源读的就是刚还原好的
1523
+ // `textures[0].url`(补过域名的形态),排在前面会用上一次项目的地址起一次错的载。
1524
+ // 旧存档没有这个键 ⇒ 空数组 = 整层清空,与上面两层的口径逐字一致。
1525
+ this.setGlassPanes(data.glass ?? []);
1526
+ }
1527
+
1528
+ /**
1529
+ * 把**存档里前 `PANO_IMPORT_PRIORITY` 个全景**的素材点名抢在别的之前加载(2026-10-29 需求)。
1530
+ * 由两处渲染循环每帧点一次:基类 `onBeforeRender`(预览弹窗 / 发布页)与编辑器的 `updateFloorTexture`
1531
+ *(`MktDevMesh` 覆写了 `onBeforeRender`,只点 `syncProjInvMatrix` + `tickProgressiveReveal`,库内那句在编辑态不跑)。
1532
+ *
1533
+ * 为什么能「先加载」:拼合器每帧那三道工序(补下载 → 画已进 GPU 的 → 再传一张)都按
1534
+ * `_groups` 这张 Map 的**在册顺序**取料,而下载额度只有「每帧 2 张 / 在途 ≤8 / 已解码待上传 ≤8」
1535
+ * (`panoTileStitcher.ts` 顶部那三个常数)⇒ 排在头部的两组把额度吃满,其余全景要等它们画满才轮得到。
1536
+ * 平时这个顺序由每帧的槽位名册定(离注视点近的先下),本方法把它钉成「存档数组的前两个」,与机位无关。
1537
+ * ⚠️⚠️ 为什么点名时要带第三个参 `front = true`(每帧把这两批**挪回队首**)而⛔ 不只是「先入册」:
1538
+ * 在册顺序是拼合器自己的状态 —— 后入册的只会**追加到队尾**,而 `ensure` 幂等、已在册就不再移动。
1539
+ * 预览这条路恰好是最坏的那种排法:`feedFaceMapping` 开头还有一记 `pump`,拼合器**第一次**拿到渲染器时
1540
+ * 必走 `_switchRenderer`(`null → 真 renderer` 也算换,弹窗每开一次就是换一次)把在册组整份作废,
1541
+ * 随后名册按距离重新入册占了队首 ⇒ 本方法下一帧重新入册只能排在它们后面,而且**从此再也翻不了身**。
1542
+ * 2026-10-29 逐帧打点就是这个形状:帧 1 `p0,p1,p11…p4` → 帧 2 起 `p11…p4,p0,p1`,
1543
+ * 40 帧下来 p0 只下到 1 片、p1 下到 30 片,离注视点最近那颗第 13 条请求就开始了 = 线上报的现象。
1544
+ * ⇒ 带 `front` 之后顺序每帧重新成立,与谁先入册、作废过几次、机位在哪都无关
1545
+ * (真跑自证在护栏 BE ④:`D` 后入册排队尾,带 `front` 再点一次 ⇒ 立刻回到队首、下一帧下载全归它)。
1546
+ * ⚠️ 为什么是**每帧**而不是「载入那一次点一记」:上面那次作废就发生在载入之后的第一帧,
1547
+ * 只在 `importData` 里点名等于白点;本方法点在 `pump` 之后、名册逐槽 `ensure` 之前,且每帧都点
1548
+ * (已在册且已在队首 ⇒ `_toFront` 里一次 `keys().next()` 就返回,没有额外开销)。
1549
+ * ⛔ 不另立第二把下载尺:这里用的就是 `feedFaceMapping` 每帧在用的同一个 `ensure(info.tiles, …)`
1550
+ * 与同一个批次键(第一条非空瓦片地址)⇒ 后面那句自然判重,一片瓦片终身只 `fetch` 一次。
1551
+ * ⚠️ 形态判据读 `this._panoTexMode`(整图形态那一档一张全景只有一张整图,走 `_texCache` 那条路,
1552
+ * 走错档就是白下 32 片)⇒ 宿主若用自己那份形态变量(编辑器的 `store.panoTexMode`),
1553
+ * 要在调 `importData` 之前先 `setPanoTexMode` 推进来,编辑器那侧已按此接线。
1554
+ * ⚠️ 代价如实记:这两颗全景若本来不在开机那一刻的名册里,它们的瓦片与拼合槽 RT 会被**提前**建出来
1555
+ * (现行口径每颗全景 2 槽 × 6144² ≈ 400MB 显存)—— 组在运行中不淘汰,走进那两个景点迟早要建,
1556
+ * 这里只是把这一笔挪到了载入的第一帧。
1557
+ */
1558
+ prefetchHeadPanoramas(): void {
1559
+ const n = Math.min(PANO_IMPORT_PRIORITY, this.textures.length);
1560
+ // ⚠️ **倒着**点:`front` 是「挪到最前」,正着点会让 0 号被 1 号压到后面(实测队首变成 `p1,p0`)
1561
+ // ⇒ 倒着点,最后一手挪的是 0 号,存档序号小的仍排在更前。
1562
+ for (let i = n - 1; i >= 0; i--) {
1563
+ const info = this.textures[i];
1564
+ // 与 `feedFaceMapping` 里那一句同判据:真瓦片才进拼合器,整图形态进瓦片缓存
1565
+ // 第三个参 `true` = 把这批**挪到在册顺序最前**(⛔ 光靠「先入册」押不住顺序,见方法头)
1566
+ if (mktPanoTiledFlag(info.tiles, this._panoTexMode) === 1) this._stitcher.ensure(info.tiles, undefined, true);
1567
+ else if (info.url) this.ensureTileTexture(info.url, false);
1568
+ }
1399
1569
  }
1400
1570
 
1401
1571
  // ───────────────────────── 释放 ─────────────────────────
1402
1572
 
1403
- /** 释放 GPU 资源:瓦片缓存(**只有这里**清,皇上旨 2026-10-02)、拼合器、平面图/模型两层、几何、材质 */
1573
+ /** 释放 GPU 资源:瓦片缓存(**只有这里**清,2026-10-02 决定)、拼合器、平面图 / 模型 / 玻璃面三层、几何、材质 */
1404
1574
  dispose(): this {
1405
1575
  // ⚠️ rt 虚拟键从不进 `_texCache`(拼合槽纹理由 `_stitcher` 自己持有并释放),这里清的都是本类下载的 Texture。
1406
1576
  // ⭐ 甲(2026-10-05):`releasePanoTexture` 除 `dispose()` 外还要 `ImageBitmap.close()` —— 位图的原生内存
@@ -1422,6 +1592,16 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1422
1592
  ms.dispose();
1423
1593
  }
1424
1594
  this.models = [];
1595
+ // 玻璃面层:几何/材质各归各颗释放;那张共用的反射纹理在这里连位图一起关
1596
+ // (⛔ 它从不在 `_texCache` 里 ⇒ 上面那一轮清不到它,漏了这一句就是每次换页漏一张全景整图)。
1597
+ for (const gp of this.glassPanes) {
1598
+ this.remove(gp);
1599
+ gp.dispose();
1600
+ }
1601
+ this.glassPanes = [];
1602
+ releasePanoTexture(this._glassEnvTex);
1603
+ this._glassEnvTex = null;
1604
+ this._glassEnvKey = "";
1425
1605
  this._imgTileLoader.clear();
1426
1606
  this._imgBindQueued.clear();
1427
1607
  this._imgBindDone.clear();
@@ -0,0 +1,43 @@
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
+ };
@@ -0,0 +1,97 @@
1
+ /*
2
+ * @Description: 正方形玻璃面 —— 2026-10-28 需求「mkteditor 还需要增加一个 正方的面的 功能, 要双面显示,
3
+ * 环境贴图就用第一个全景, 效果用 three.js 的 webgl_materials_physical_transmission 案例, 并且要写入 json 数据和回显」。
4
+ *
5
+ * 定位:与「图片平面」(`./imagePlane.ts`)同为挂在 `MktMesh` 子树下的独立 Mesh,但材质完全不同:
6
+ * - 图片平面 = MeshBasicMaterial + FrontSide + 自带内容瓦片(单面可见是 2026-10-22 单独点名的,⛔ 不适用于本类);
7
+ * - 玻璃面 = MeshPhysicalMaterial + DoubleSide + transmission(本次点名「要双面显示」⇒ 正反两面都画、都折射)。
8
+ * 它自己没有内容纹理:画面 = 背后的东西被折射 + 一层反射,反射来自 `MktMesh` 给的**第一个全景**(`setEnvironmentTexture`)。
9
+ *
10
+ * 材质参数逐字对齐官方示例 `webgl_materials_physical_transmission` 的默认档
11
+ * (transmission 1 / ior 1.5 / thickness 0.01 / roughness 0 / metalness 0 / specularIntensity 1 /
12
+ * specularColor 白 / envMapIntensity 1 / color 白 / opacity 1 / transparent true / side DoubleSide)。
13
+ * ⛔ 示例 GUI 里那颗 `transmissionResolutionScale` 在 three 0.167 的构建里不存在(属主渲染器且该版本无此属性),
14
+ * 写了会直接把类型检查打红 ⇒ 本文件与宿主都不设它,折射全屏缓冲用引擎默认分辨率。
15
+ * ⛔ 不设 `alphaMap`:示例把它挂在那颗圆环上(切面形状用),玻璃面就是一块方板,没有alpha 轮廓要描。
16
+ *
17
+ * 形状:正方形 ⇒ 存档只有**一个**边长 `size`(米),宽恒等于高。几何仍是 PlaneGeometry(size, size),
18
+ * 原点在板中心,落地/贴墙那两种落点由调用者摆(与图片平面同一条口径,见编辑器 `addGlassPaneAt`)。
19
+ */
20
+ import { Color, DoubleSide, Mesh, MeshPhysicalMaterial, PlaneGeometry } from "three";
21
+ const DEFAULT_SIZE = 1.5;
22
+ /** 选中高亮色:与图片平面/模型槽同一档(同族观感一致,只换颜色不换形态) */
23
+ const HIGHLIGHT_COLOR = "#ffd24a";
24
+ export class GlassPane extends Mesh {
25
+ _originalColor = "#ffffff";
26
+ /** 反射环境纹理的身份(由 `MktMesh` 注入,本类只持有引用、⛔ 从不 dispose 它 —— 那张按地址缓存在库里) */
27
+ _envTex = null;
28
+ constructor(name) {
29
+ const geo = new PlaneGeometry(DEFAULT_SIZE, DEFAULT_SIZE);
30
+ const mat = new MeshPhysicalMaterial({
31
+ color: new Color("#ffffff"),
32
+ // 双面(本次点名「要双面显示」):正面反面都画,也都能被射线打中
33
+ //(three 的射线拾取按材质 side 剔背面,见 `node_modules/three/src/objects/Mesh.js`)。
34
+ side: DoubleSide,
35
+ transparent: true,
36
+ opacity: 1,
37
+ // ↓ 以下六档 + 上面两档 = 官方示例的默认 GUI 值,一字未改
38
+ transmission: 1,
39
+ ior: 1.5,
40
+ thickness: 0.01,
41
+ roughness: 0,
42
+ metalness: 0,
43
+ specularIntensity: 1,
44
+ specularColor: new Color("#ffffff"),
45
+ envMapIntensity: 1,
46
+ });
47
+ super(geo, mat);
48
+ this.name = name || "玻璃面";
49
+ }
50
+ get paneSize() { return this.geometry.parameters.width; }
51
+ /** 换正方形边长(宽高等比跟着走:几何只有这一把尺) */
52
+ setPaneSize(size) {
53
+ this.geometry.dispose();
54
+ this.geometry = new PlaneGeometry(size, size);
55
+ }
56
+ /**
57
+ * 绑/摘反射环境纹理(`null` = 还没有可用的环境源)。
58
+ * ⚠️ 判等在前:`material.needsUpdate = true` 会涨材质版本 ⇒ 程序要重编译。库里给同一批玻璃面注入的是
59
+ * **同一个 Texture 对象**(反射源只下载一次),逐颗重复赋同一张时必须早退,不然就是 N 次无谓重编译。
60
+ * ⚠️ 传进来的纹理必须已标 `EquirectangularReflectionMapping`:three 的 `WebGLCubeUVMaps` 只认那个标记才会
61
+ * 自动把它卷积成立方 UV(PMREM);直接给一张普通 UV 纹理(例如全景拼合槽的那两张 RT)采出来是花屏。
62
+ */
63
+ setEnvironmentTexture(tex) {
64
+ if (this._envTex === tex)
65
+ return;
66
+ this._envTex = tex;
67
+ this.material.envMap = tex;
68
+ this.material.needsUpdate = true;
69
+ }
70
+ highlight() {
71
+ this.material.color.set(HIGHLIGHT_COLOR);
72
+ }
73
+ restoreColor() {
74
+ this.material.color.set(this._originalColor);
75
+ }
76
+ toData() {
77
+ return {
78
+ name: this.name || undefined,
79
+ position: { x: this.position.x, y: this.position.y, z: this.position.z },
80
+ rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
81
+ size: this.paneSize,
82
+ };
83
+ }
84
+ applyData(d) {
85
+ if (d.name)
86
+ this.name = d.name;
87
+ this.position.set(d.position.x, d.position.y, d.position.z);
88
+ this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
89
+ this.setPaneSize(d.size);
90
+ }
91
+ /** 只释放自己的几何与材质;反射纹理归 `MktMesh` 管(多颗共用一张)。 */
92
+ dispose() {
93
+ this.geometry.dispose();
94
+ this.material.dispose();
95
+ }
96
+ }
97
+ export const GLASS_PANE_DEFAULTS = { size: DEFAULT_SIZE };
@@ -0,0 +1,116 @@
1
+ /*
2
+ * @Description: 正方形玻璃面 —— 2026-10-28 需求「mkteditor 还需要增加一个 正方的面的 功能, 要双面显示,
3
+ * 环境贴图就用第一个全景, 效果用 three.js 的 webgl_materials_physical_transmission 案例, 并且要写入 json 数据和回显」。
4
+ *
5
+ * 定位:与「图片平面」(`./imagePlane.ts`)同为挂在 `MktMesh` 子树下的独立 Mesh,但材质完全不同:
6
+ * - 图片平面 = MeshBasicMaterial + FrontSide + 自带内容瓦片(单面可见是 2026-10-22 单独点名的,⛔ 不适用于本类);
7
+ * - 玻璃面 = MeshPhysicalMaterial + DoubleSide + transmission(本次点名「要双面显示」⇒ 正反两面都画、都折射)。
8
+ * 它自己没有内容纹理:画面 = 背后的东西被折射 + 一层反射,反射来自 `MktMesh` 给的**第一个全景**(`setEnvironmentTexture`)。
9
+ *
10
+ * 材质参数逐字对齐官方示例 `webgl_materials_physical_transmission` 的默认档
11
+ * (transmission 1 / ior 1.5 / thickness 0.01 / roughness 0 / metalness 0 / specularIntensity 1 /
12
+ * specularColor 白 / envMapIntensity 1 / color 白 / opacity 1 / transparent true / side DoubleSide)。
13
+ * ⛔ 示例 GUI 里那颗 `transmissionResolutionScale` 在 three 0.167 的构建里不存在(属主渲染器且该版本无此属性),
14
+ * 写了会直接把类型检查打红 ⇒ 本文件与宿主都不设它,折射全屏缓冲用引擎默认分辨率。
15
+ * ⛔ 不设 `alphaMap`:示例把它挂在那颗圆环上(切面形状用),玻璃面就是一块方板,没有alpha 轮廓要描。
16
+ *
17
+ * 形状:正方形 ⇒ 存档只有**一个**边长 `size`(米),宽恒等于高。几何仍是 PlaneGeometry(size, size),
18
+ * 原点在板中心,落地/贴墙那两种落点由调用者摆(与图片平面同一条口径,见编辑器 `addGlassPaneAt`)。
19
+ */
20
+ import { Color, DoubleSide, Mesh, MeshPhysicalMaterial, PlaneGeometry, Texture } from "three";
21
+
22
+ /** 玻璃面的持久化数据(进 `MktMesh.exportData` 的 `glass` 键 / `importData` 还原) */
23
+ export interface GlassPaneData {
24
+ name?: string;
25
+ position: { x: number; y: number; z: number };
26
+ rotation: { x: number; y: number; z: number };
27
+ /** 正方形边长(米):宽 = 高 = 本值,⛔ 不拆成两档 */
28
+ size: number;
29
+ }
30
+
31
+ const DEFAULT_SIZE = 1.5;
32
+ /** 选中高亮色:与图片平面/模型槽同一档(同族观感一致,只换颜色不换形态) */
33
+ const HIGHLIGHT_COLOR = "#ffd24a";
34
+
35
+ export class GlassPane extends Mesh<PlaneGeometry, MeshPhysicalMaterial> {
36
+ private _originalColor: string = "#ffffff";
37
+ /** 反射环境纹理的身份(由 `MktMesh` 注入,本类只持有引用、⛔ 从不 dispose 它 —— 那张按地址缓存在库里) */
38
+ private _envTex: Texture | null = null;
39
+
40
+ constructor(name?: string) {
41
+ const geo = new PlaneGeometry(DEFAULT_SIZE, DEFAULT_SIZE);
42
+ const mat = new MeshPhysicalMaterial({
43
+ color: new Color("#ffffff"),
44
+ // 双面(本次点名「要双面显示」):正面反面都画,也都能被射线打中
45
+ //(three 的射线拾取按材质 side 剔背面,见 `node_modules/three/src/objects/Mesh.js`)。
46
+ side: DoubleSide,
47
+ transparent: true,
48
+ opacity: 1,
49
+ // ↓ 以下六档 + 上面两档 = 官方示例的默认 GUI 值,一字未改
50
+ transmission: 1,
51
+ ior: 1.5,
52
+ thickness: 0.01,
53
+ roughness: 0,
54
+ metalness: 0,
55
+ specularIntensity: 1,
56
+ specularColor: new Color("#ffffff"),
57
+ envMapIntensity: 1,
58
+ });
59
+ super(geo, mat);
60
+ this.name = name || "玻璃面";
61
+ }
62
+
63
+ get paneSize(): number { return (this.geometry as PlaneGeometry).parameters.width; }
64
+
65
+ /** 换正方形边长(宽高等比跟着走:几何只有这一把尺) */
66
+ setPaneSize(size: number): void {
67
+ this.geometry.dispose();
68
+ this.geometry = new PlaneGeometry(size, size);
69
+ }
70
+
71
+ /**
72
+ * 绑/摘反射环境纹理(`null` = 还没有可用的环境源)。
73
+ * ⚠️ 判等在前:`material.needsUpdate = true` 会涨材质版本 ⇒ 程序要重编译。库里给同一批玻璃面注入的是
74
+ * **同一个 Texture 对象**(反射源只下载一次),逐颗重复赋同一张时必须早退,不然就是 N 次无谓重编译。
75
+ * ⚠️ 传进来的纹理必须已标 `EquirectangularReflectionMapping`:three 的 `WebGLCubeUVMaps` 只认那个标记才会
76
+ * 自动把它卷积成立方 UV(PMREM);直接给一张普通 UV 纹理(例如全景拼合槽的那两张 RT)采出来是花屏。
77
+ */
78
+ setEnvironmentTexture(tex: Texture | null): void {
79
+ if (this._envTex === tex) return;
80
+ this._envTex = tex;
81
+ this.material.envMap = tex;
82
+ this.material.needsUpdate = true;
83
+ }
84
+
85
+ highlight(): void {
86
+ this.material.color.set(HIGHLIGHT_COLOR);
87
+ }
88
+
89
+ restoreColor(): void {
90
+ this.material.color.set(this._originalColor);
91
+ }
92
+
93
+ toData(): GlassPaneData {
94
+ return {
95
+ name: this.name || undefined,
96
+ position: { x: this.position.x, y: this.position.y, z: this.position.z },
97
+ rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
98
+ size: this.paneSize,
99
+ };
100
+ }
101
+
102
+ applyData(d: GlassPaneData): void {
103
+ if (d.name) this.name = d.name;
104
+ this.position.set(d.position.x, d.position.y, d.position.z);
105
+ this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
106
+ this.setPaneSize(d.size);
107
+ }
108
+
109
+ /** 只释放自己的几何与材质;反射纹理归 `MktMesh` 管(多颗共用一张)。 */
110
+ dispose(): void {
111
+ this.geometry.dispose();
112
+ this.material.dispose();
113
+ }
114
+ }
115
+
116
+ export const GLASS_PANE_DEFAULTS = { size: DEFAULT_SIZE };
@@ -16,8 +16,11 @@ import { Texture } from "three";
16
16
  * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
17
17
  */
18
18
  /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
19
- * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
20
- * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
19
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(用户决定 2026-09-25),
20
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。
21
+ * ⭐ 这一步走的是原生 `fetch`,不经过 three 的 `Loader`,所以它**自己**向 `DefaultLoadingManager`
22
+ * 报一次 `itemStart` / `itemEnd`(失败再多一次 `itemError`,与 `FileLoader` 的口径一致)——
23
+ * 否则页面拿到的加载状态只算模型和 `TextureLoader` 那些路,全景切片的下载完全不在里面。 */
21
24
  export declare function loadPanoTexture(url: string): Promise<Texture>;
22
25
  /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
23
26
  * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
@@ -1,4 +1,4 @@
1
- import { Texture } from "three";
1
+ import { DefaultLoadingManager, Texture } from "three";
2
2
  /**
3
3
  * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
4
4
  *
@@ -16,10 +16,14 @@ import { Texture } from "three";
16
16
  * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
17
17
  */
18
18
  /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
19
- * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
20
- * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
19
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(用户决定 2026-09-25),
20
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。
21
+ * ⭐ 这一步走的是原生 `fetch`,不经过 three 的 `Loader`,所以它**自己**向 `DefaultLoadingManager`
22
+ * 报一次 `itemStart` / `itemEnd`(失败再多一次 `itemError`,与 `FileLoader` 的口径一致)——
23
+ * 否则页面拿到的加载状态只算模型和 `TextureLoader` 那些路,全景切片的下载完全不在里面。 */
21
24
  export function loadPanoTexture(url) {
22
- return fetch(url)
25
+ DefaultLoadingManager.itemStart(url);
26
+ const task = fetch(url)
23
27
  .then((res) => {
24
28
  if (!res.ok)
25
29
  throw new Error(`HTTP ${res.status}`);
@@ -38,6 +42,12 @@ export function loadPanoTexture(url) {
38
42
  tex.userData.url = url;
39
43
  return tex;
40
44
  });
45
+ // 一条 promise 只 settle 一次 ⇒ 开始/结束各报一次,两边都不会漏;reject 原样透给调用方。
46
+ task.then(() => DefaultLoadingManager.itemEnd(url), () => {
47
+ DefaultLoadingManager.itemError(url);
48
+ DefaultLoadingManager.itemEnd(url);
49
+ });
50
+ return task;
41
51
  }
42
52
  /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
43
53
  * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
@@ -1,4 +1,4 @@
1
- import { Texture } from "three";
1
+ import { DefaultLoadingManager, Texture } from "three";
2
2
 
3
3
  /**
4
4
  * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
@@ -18,10 +18,14 @@ import { Texture } from "three";
18
18
  */
19
19
 
20
20
  /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
21
- * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
22
- * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
21
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(用户决定 2026-09-25),
22
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。
23
+ * ⭐ 这一步走的是原生 `fetch`,不经过 three 的 `Loader`,所以它**自己**向 `DefaultLoadingManager`
24
+ * 报一次 `itemStart` / `itemEnd`(失败再多一次 `itemError`,与 `FileLoader` 的口径一致)——
25
+ * 否则页面拿到的加载状态只算模型和 `TextureLoader` 那些路,全景切片的下载完全不在里面。 */
23
26
  export function loadPanoTexture(url: string): Promise<Texture> {
24
- return fetch(url)
27
+ DefaultLoadingManager.itemStart(url);
28
+ const task = fetch(url)
25
29
  .then((res) => {
26
30
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
27
31
  return res.blob();
@@ -41,6 +45,15 @@ export function loadPanoTexture(url: string): Promise<Texture> {
41
45
  tex.userData.url = url;
42
46
  return tex;
43
47
  });
48
+ // 一条 promise 只 settle 一次 ⇒ 开始/结束各报一次,两边都不会漏;reject 原样透给调用方。
49
+ task.then(
50
+ () => DefaultLoadingManager.itemEnd(url),
51
+ () => {
52
+ DefaultLoadingManager.itemError(url);
53
+ DefaultLoadingManager.itemEnd(url);
54
+ }
55
+ );
56
+ return task;
44
57
  }
45
58
 
46
59
  /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
@@ -7,8 +7,11 @@ import { type TileLayout } from "./mktShader";
7
7
  export interface PanoStitcher {
8
8
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
9
9
  * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
10
- * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。 */
11
- ensure(urls: string[], layout?: TileLayout): void;
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;
12
15
  /**
13
16
  * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
14
17
  * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
@@ -74,7 +77,10 @@ export declare class PanoTileStitcher implements PanoStitcher {
74
77
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
75
78
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
76
79
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
77
- ensure(urls: string[], layout?: TileLayout): void;
80
+ ensure(urls: string[], layout?: TileLayout, front?: boolean): void;
81
+ /** 把一批挪到在册顺序的**最前**(只动 Map 的顺序,⛔ 不动组里任何东西:RT、已画格子、瓦片账本原样)。
82
+ * 已在最前 ⇒ 一次 `keys().next()` 就返回(每帧点名走的就是这条零成本支路)。 */
83
+ private _toFront;
78
84
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
79
85
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
80
86
  peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
@@ -220,7 +220,7 @@ export class PanoTileStitcher {
220
220
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
221
221
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
222
222
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
223
- ensure(urls, layout) {
223
+ ensure(urls, layout, front = false) {
224
224
  const L = layout ?? PANO_TILE_LAYOUT;
225
225
  const maxTiles = tilesPerBatchOf(L);
226
226
  if (!urls || urls.length === 0 || urls.length > maxTiles)
@@ -229,8 +229,11 @@ export class PanoTileStitcher {
229
229
  while (u.length < maxTiles)
230
230
  u.push("");
231
231
  const batch = rtStitchBatchOf(u);
232
- if (this._groups.has(batch))
232
+ if (this._groups.has(batch)) {
233
+ if (front)
234
+ this._toFront(batch);
233
235
  return;
236
+ }
234
237
  const slotCount = L.slotCount;
235
238
  const cellsPerSlot = cellsPerSlotOf(L);
236
239
  this._groups.set(batch, {
@@ -244,8 +247,26 @@ export class PanoTileStitcher {
244
247
  drawnCount: Array.from({ length: slotCount }, () => 0),
245
248
  pending: slotCount,
246
249
  });
250
+ if (front)
251
+ this._toFront(batch);
247
252
  this._stamp++;
248
253
  }
254
+ /** 把一批挪到在册顺序的**最前**(只动 Map 的顺序,⛔ 不动组里任何东西:RT、已画格子、瓦片账本原样)。
255
+ * 已在最前 ⇒ 一次 `keys().next()` 就返回(每帧点名走的就是这条零成本支路)。 */
256
+ _toFront(batch) {
257
+ const g = this._groups.get(batch);
258
+ if (!g)
259
+ 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);
269
+ }
249
270
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
250
271
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
251
272
  peek(batch, slot, renderer) {
@@ -238,8 +238,11 @@ interface StitchGroup {
238
238
  export interface PanoStitcher {
239
239
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
240
240
  * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
241
- * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。 */
242
- ensure(urls: string[], layout?: TileLayout): void;
241
+ * 图片平面传 `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;
243
246
  /**
244
247
  * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
245
248
  * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
@@ -326,14 +329,17 @@ private _uploadQueued: Set<TileRec> = new Set();
326
329
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
327
330
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
328
331
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
329
- ensure(urls: string[], layout?: TileLayout): void {
332
+ ensure(urls: string[], layout?: TileLayout, front = false): void {
330
333
  const L = layout ?? PANO_TILE_LAYOUT;
331
334
  const maxTiles = tilesPerBatchOf(L);
332
335
  if (!urls || urls.length === 0 || urls.length > maxTiles) return;
333
336
  const u = urls.slice();
334
337
  while (u.length < maxTiles) u.push("");
335
338
  const batch = rtStitchBatchOf(u);
336
- if (this._groups.has(batch)) return;
339
+ if (this._groups.has(batch)) {
340
+ if (front) this._toFront(batch);
341
+ return;
342
+ }
337
343
  const slotCount = L.slotCount;
338
344
  const cellsPerSlot = cellsPerSlotOf(L);
339
345
  this._groups.set(batch, {
@@ -347,9 +353,23 @@ private _uploadQueued: Set<TileRec> = new Set();
347
353
  drawnCount: Array.from({ length: slotCount }, () => 0),
348
354
  pending: slotCount,
349
355
  });
356
+ if (front) this._toFront(batch);
350
357
  this._stamp++;
351
358
  }
352
359
 
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);
371
+ }
372
+
353
373
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
354
374
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
355
375
  peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null {
@@ -1,6 +1,7 @@
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";
4
5
  /**
5
6
  * 单个纹理的运行时信息:url + 矩阵(位置 + 旋转,缩放忽略)。
6
7
  * ⚠️ 这个矩阵记在 MktMesh **自身局部(制图)空间**,与 `wallPoints` 同一空间 —— 不是世界矩阵:
@@ -87,4 +88,9 @@ export interface MktMeshData {
87
88
  images?: ImagePlaneData[];
88
89
  /** 模型槽层(同一道旨):`url` 是 .glb 的相对地址;空串 = 还没上传 ⇒ 显示占位小立方体 */
89
90
  models?: ModelSlotData[];
91
+ /**
92
+ * 玻璃面层(2026-10-28 需求「正方的面」):正方形、双面、transmission 折射,反射环境取运行时第一个全景。
93
+ * ⛔ 这一层不落任何素材地址 ⇒ 既不过 `stripAssetHost` 也不过 `absAssetUrl`,只有位置/旋转/边长。
94
+ */
95
+ glass?: GlassPaneData[];
90
96
  }
package/MktMesh/types.ts CHANGED
@@ -8,6 +8,7 @@
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";
11
12
 
12
13
  /**
13
14
  * 单个纹理的运行时信息:url + 矩阵(位置 + 旋转,缩放忽略)。
@@ -99,4 +100,9 @@ export interface MktMeshData {
99
100
  images?: ImagePlaneData[];
100
101
  /** 模型槽层(同一道旨):`url` 是 .glb 的相对地址;空串 = 还没上传 ⇒ 显示占位小立方体 */
101
102
  models?: ModelSlotData[];
103
+ /**
104
+ * 玻璃面层(2026-10-28 需求「正方的面」):正方形、双面、transmission 折射,反射环境取运行时第一个全景。
105
+ * ⛔ 这一层不落任何素材地址 ⇒ 既不过 `stripAssetHost` 也不过 `absAssetUrl`,只有位置/旋转/边长。
106
+ */
107
+ glass?: GlassPaneData[];
102
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-base",
3
- "version": "2.0.192",
3
+ "version": "2.0.193",
4
4
  "description": "A Three.js extension library providing common 3D components, animations, materials, and utilities",
5
5
  "author": "xiaosihan",
6
6
  "publishConfig": {
@@ -47,6 +47,11 @@
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
+ },
50
55
  "./MktMesh/boxLine2": {
51
56
  "types": "./MktMesh/boxLine2.d.ts",
52
57
  "import": "./MktMesh/boxLine2.js",
package/threeLoader.js CHANGED
@@ -28,6 +28,8 @@ class ThreeLoader extends EventDispatcher {
28
28
  };
29
29
  // 资源加载进度
30
30
  DefaultLoadingManager.onProgress = (url, loaded, total) => {
31
+ // 三个数一并转发:首页只认「还在不在加载」,弹窗一类要看进度
32
+ this.dispatchEvent({ type: "progress", url, loaded, total });
31
33
  if (loaded < total) {
32
34
  this.dispatchEvent({ type: "loadding" });
33
35
  }
package/threeLoader.ts CHANGED
@@ -32,6 +32,8 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
32
32
  };
33
33
  // 资源加载进度
34
34
  DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => {
35
+ // 三个数一并转发:首页只认「还在不在加载」,弹窗一类要看进度
36
+ this.dispatchEvent({ type: "progress", url, loaded, total });
35
37
  if (loaded < total) {
36
38
  this.dispatchEvent({ type: "loadding" });
37
39
  }