three-base 2.0.181 → 2.0.188

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.
@@ -130,9 +130,23 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
130
130
  models: ModelSlot[];
131
131
  /**
132
132
  * 那 4 张瓦片的下载/解码缓存。⭐ 与全景 `_texCache` 同一条旨(10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」)
133
- * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不闪缺图灰;只随本对象 `dispose()` 释放。
133
+ * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不会再空一格;只随本对象 `dispose()` 释放。
134
134
  */
135
135
  private _imgTileLoader;
136
+ /**
137
+ * 逐帧绑定用的两张账(2026-10-26 定的方案:地址一次性全部丢给下载,绑材质改成「一帧一颗平面」)。
138
+ * 全局 rAF 每拍只跑一项 `addWork`,而 `addWork` 按闭包引用判重 ⇒ 每次现造的新闭包它判不出来,
139
+ * ⛔ 去重闸门必须像全景上传那张 `_uploadQueued` 一样自备。
140
+ */
141
+ private _imgBindQueued;
142
+ /** 平面 → 「它 4 条地址全部落定(就绪或失败)时的那串地址」⇒ 地址不换就不再重排;上传换图会改这串 ⇒ 自动重排 */
143
+ private _imgBindDone;
144
+ /**
145
+ * 模型槽「GLB 已取回、等这一帧把它挂进父元素」的在册表 —— 与上面图片绑定同一手法(用户 10-26 需求:
146
+ * 一次性全部拿去加载,用 `threeUtils.addWork` 逐帧加到场景里)。整层十几个槽同时到货时,
147
+ * 一帧只有一颗进 `_contentRoot`(进图那一拍要重算包围盒 + 排布 + 首次把几何送上 GPU)。
148
+ */
149
+ private _modelMountQueued;
136
150
  constructor(opts?: {
137
151
  wallHeight?: number;
138
152
  wallClosed?: boolean;
@@ -309,13 +323,13 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
309
323
  projectName: string;
310
324
  /** 注入项目名(导出 json 前调一次即可;导入时无须调——还原靠落盘地址自身的首段) */
311
325
  setProjectName(name: string): void;
312
- /** 单颗图片平面入层(编辑态双击落点用):进数组 + 挂进本对象子树 */
326
+ /** 单颗图片平面入层(编辑态双击落点用):进数组 + 挂进本对象子树 + 起排它自己的逐帧绑定 */
313
327
  addImagePlane(ip: ImagePlane): ImagePlane;
314
328
  /** 摘掉一颗并释放它自己的几何/材质;那 4 张瓦片纹理归 `_imgTileLoader` 管 ⇒ ⛔ 不在这里 dispose */
315
329
  removeImagePlane(ip: ImagePlane): void;
316
330
  /** 单颗模型槽入层(编辑态双击落点、以及「复制模型」用) */
317
331
  addModel(ms: ModelSlot): ModelSlot;
318
- /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾) */
332
+ /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾;排在后头的挂载帧靠「不在册」早退) */
319
333
  removeModel(ms: ModelSlot): void;
320
334
  /**
321
335
  * 整层重建图片平面(`importData` 与预览那一路)。
@@ -327,14 +341,30 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
327
341
  * 整层重建模型槽(`importData` 与预览那一路),地址口径的要求同 `setImagePlanes`。
328
342
  * ⚠️ GLB 逐只**异步取、不 await**(十几只串一条队会把载入卡死),每只落地那一刻各回调一次 `onEachLoad`:
329
343
  * 预览每帧真画 ⇒ 不传;编辑器是按需渲染 ⇒ 传一个「标脏」的钩子,否则模型挂上来屏幕不动。
344
+ * 2026-10-26 决定:取回与「挂进父元素」分两拍 —— 地址一次性全交给下载(网络 + 解析不占渲染帧),
345
+ * 真正进场景图那一步交给 `threeUtils.addWork` ⇒ 全局 rAF 每拍只跑一项 = **一帧只挂一颗槽**
346
+ * (挂载那一拍要重算包围盒 + 排布 + 首次把整棵子树的几何送上 GPU,十几颗挤在同一帧就是他要拆掉的那一卡)。
330
347
  */
331
348
  setModels(datas: ModelSlotData[], onEachLoad?: () => void): void;
349
+ /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
350
+ private _queueModelMount;
351
+ /** 全局队列里的那一帧:只挂这一槽,挂好才回调(`onEachLoad` = 按需渲染宿主的标脏钩子) */
352
+ private _mountModelStep;
332
353
  /**
333
- * 每帧把各颗平面那 4 张瓦片的纹理直采绑到材质(⭐ 10-22 晚旨:不走拼合器 RT)。
334
- * 库里的**唯一驱动点**:持有渲染循环的那一方(编辑态 / 预览 / 下游)每帧调一次,⛔ 不许在调用方再抄一份循环。
335
- * 还没下载完/缺格的那一格绑 null ⇒ 片元画缺图灰(皇上 10-26 收回默认图那条旨:「不要默认图, 没有加到位的时候 就显示深灰色」)。
354
+ * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
355
+ * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
356
+ * 真正写 uniform 的那一步交给 `threeUtils.addWork` —— 全局 rAF 每拍只跑一项 ⇒ 一帧只绑一颗平面,
357
+ * 一整层平面的纹理上传被摊到多帧,不再挤在同一记调用里(旧写法一记 `feed` 就把所有平面全绑完)。
358
+ * ⛔ 不进 `onBeforeRender` 自驱;持有渲染循环的那一方(编辑态 / 预览 / 下游)每帧调一次只是「补排漏网的平面」,
359
+ * 而整层重建(`setImagePlanes`)末尾自己起排 ⇒ 只调 `importData(整串)` 的宿主不必自己驱动也会长出来。
360
+ * 还没下载完/缺格的那一格绑 null ⇒ 片元那一格整格 discard,不落像素(用户 10-26 连收两条:先「不要默认图」,
361
+ * 再「不是显示灰色, 而是直接 discard 丢弃」)。
336
362
  */
337
363
  feedImagePlaneTiles(): void;
364
+ /** 一颗平面排一帧的绑定工作:先把它那 4 条地址**一次性全部**交给下载器,再把「写材质」排进全局队列。 */
365
+ private _queueImagePlaneBind;
366
+ /** 队列里的一格:只绑**这一颗**平面里已就绪的那几格;还有在途的就重新排队,⛔ 不在这里等、也⛔ 不顺手把别的平面一起做完。 */
367
+ private _bindImagePlaneStep;
338
368
  /** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
339
369
  * @param projName 项目名(可选,优先于 `setProjectName` 注入的那个)——图片地址落成 `{项目名}/xxx.jpg` */
340
370
  exportData(projName?: string): MktMeshData;
@@ -187,9 +187,23 @@ export default class MktMesh extends Mesh {
187
187
  models = [];
188
188
  /**
189
189
  * 那 4 张瓦片的下载/解码缓存。⭐ 与全景 `_texCache` 同一条旨(10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」)
190
- * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不闪缺图灰;只随本对象 `dispose()` 释放。
190
+ * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不会再空一格;只随本对象 `dispose()` 释放。
191
191
  */
192
192
  _imgTileLoader = new ImagePlaneTileLoader();
193
+ /**
194
+ * 逐帧绑定用的两张账(2026-10-26 定的方案:地址一次性全部丢给下载,绑材质改成「一帧一颗平面」)。
195
+ * 全局 rAF 每拍只跑一项 `addWork`,而 `addWork` 按闭包引用判重 ⇒ 每次现造的新闭包它判不出来,
196
+ * ⛔ 去重闸门必须像全景上传那张 `_uploadQueued` 一样自备。
197
+ */
198
+ _imgBindQueued = new Set();
199
+ /** 平面 → 「它 4 条地址全部落定(就绪或失败)时的那串地址」⇒ 地址不换就不再重排;上传换图会改这串 ⇒ 自动重排 */
200
+ _imgBindDone = new Map();
201
+ /**
202
+ * 模型槽「GLB 已取回、等这一帧把它挂进父元素」的在册表 —— 与上面图片绑定同一手法(用户 10-26 需求:
203
+ * 一次性全部拿去加载,用 `threeUtils.addWork` 逐帧加到场景里)。整层十几个槽同时到货时,
204
+ * 一帧只有一颗进 `_contentRoot`(进图那一拍要重算包围盒 + 排布 + 首次把几何送上 GPU)。
205
+ */
206
+ _modelMountQueued = new Set();
193
207
  constructor(opts = {}) {
194
208
  // 发布/预览态墙:**固定**用「全景序号直取」着色器(皇上旨 2026-09-29:mktmesh 就只有按照 attribute
195
209
  // 配置来显示全景这一种固定的 shader——每帧由 feedFaceMapping 按 panoFaceMapping 把被引用的全景
@@ -1027,10 +1041,11 @@ export default class MktMesh extends Mesh {
1027
1041
  this.projectName = typeof name === "string" ? name : "";
1028
1042
  }
1029
1043
  // ───────────── 平面图 / 模型两层(⭐ 皇上旨 2026-10-26「把图片平面和模型一起搬进库里」) ─────────────
1030
- /** 单颗图片平面入层(编辑态双击落点用):进数组 + 挂进本对象子树 */
1044
+ /** 单颗图片平面入层(编辑态双击落点用):进数组 + 挂进本对象子树 + 起排它自己的逐帧绑定 */
1031
1045
  addImagePlane(ip) {
1032
1046
  this.imagePlanes.push(ip);
1033
1047
  this.add(ip);
1048
+ this._queueImagePlaneBind(ip);
1034
1049
  return ip;
1035
1050
  }
1036
1051
  /** 摘掉一颗并释放它自己的几何/材质;那 4 张瓦片纹理归 `_imgTileLoader` 管 ⇒ ⛔ 不在这里 dispose */
@@ -1038,6 +1053,8 @@ export default class MktMesh extends Mesh {
1038
1053
  const i = this.imagePlanes.indexOf(ip);
1039
1054
  if (i >= 0)
1040
1055
  this.imagePlanes.splice(i, 1);
1056
+ this._imgBindQueued.delete(ip);
1057
+ this._imgBindDone.delete(ip);
1041
1058
  this.remove(ip);
1042
1059
  ip.dispose();
1043
1060
  }
@@ -1047,11 +1064,12 @@ export default class MktMesh extends Mesh {
1047
1064
  this.add(ms);
1048
1065
  return ms;
1049
1066
  }
1050
- /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾) */
1067
+ /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾;排在后头的挂载帧靠「不在册」早退) */
1051
1068
  removeModel(ms) {
1052
1069
  const i = this.models.indexOf(ms);
1053
1070
  if (i >= 0)
1054
1071
  this.models.splice(i, 1);
1072
+ this._modelMountQueued.delete(ms);
1055
1073
  this.remove(ms);
1056
1074
  ms.dispose();
1057
1075
  }
@@ -1061,6 +1079,8 @@ export default class MktMesh extends Mesh {
1061
1079
  * ⛔ 本方法里不许再补一遍,也⛔ 不许在调用方各自补。
1062
1080
  */
1063
1081
  setImagePlanes(datas) {
1082
+ this._imgBindQueued.clear();
1083
+ this._imgBindDone.clear();
1064
1084
  for (const ip of this.imagePlanes) {
1065
1085
  this.remove(ip);
1066
1086
  ip.dispose();
@@ -1072,13 +1092,19 @@ export default class MktMesh extends Mesh {
1072
1092
  this.imagePlanes.push(ip);
1073
1093
  this.add(ip);
1074
1094
  }
1095
+ // 整层重建完立刻起排:地址一次性全交给下载、绑材质逐帧一记 ⇒ 宿主不驱动这一层也长得出来
1096
+ this.feedImagePlaneTiles();
1075
1097
  }
1076
1098
  /**
1077
1099
  * 整层重建模型槽(`importData` 与预览那一路),地址口径的要求同 `setImagePlanes`。
1078
1100
  * ⚠️ GLB 逐只**异步取、不 await**(十几只串一条队会把载入卡死),每只落地那一刻各回调一次 `onEachLoad`:
1079
1101
  * 预览每帧真画 ⇒ 不传;编辑器是按需渲染 ⇒ 传一个「标脏」的钩子,否则模型挂上来屏幕不动。
1102
+ * 2026-10-26 决定:取回与「挂进父元素」分两拍 —— 地址一次性全交给下载(网络 + 解析不占渲染帧),
1103
+ * 真正进场景图那一步交给 `threeUtils.addWork` ⇒ 全局 rAF 每拍只跑一项 = **一帧只挂一颗槽**
1104
+ * (挂载那一拍要重算包围盒 + 排布 + 首次把整棵子树的几何送上 GPU,十几颗挤在同一帧就是他要拆掉的那一卡)。
1080
1105
  */
1081
1106
  setModels(datas, onEachLoad) {
1107
+ this._modelMountQueued.clear();
1082
1108
  for (const ms of this.models) {
1083
1109
  this.remove(ms);
1084
1110
  ms.dispose();
@@ -1088,42 +1114,93 @@ export default class MktMesh extends Mesh {
1088
1114
  const ms = new ModelSlot(d.name);
1089
1115
  ms.applyData(d);
1090
1116
  this.models.push(ms);
1091
- this.add(ms);
1117
+ this.add(ms); // 槽本体(占位小立方体)当场进图 ⇒ GLB 在路上时这一槽不凭空消失
1092
1118
  if (d.url)
1093
- void ms.setModelUrl(d.url).then(() => onEachLoad && onEachLoad());
1119
+ void ms.loadModel(d.url).then(() => this._queueModelMount(ms, onEachLoad));
1094
1120
  }
1095
1121
  }
1122
+ /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
1123
+ _queueModelMount(ms, onEachLoad) {
1124
+ if (this._modelMountQueued.has(ms))
1125
+ return;
1126
+ this._modelMountQueued.add(ms);
1127
+ threeUtils.addWork(() => { });
1128
+ threeUtils.addWork(() => this._mountModelStep(ms, onEachLoad));
1129
+ }
1130
+ /** 全局队列里的那一帧:只挂这一槽,挂好才回调(`onEachLoad` = 按需渲染宿主的标脏钩子) */
1131
+ _mountModelStep(ms, onEachLoad) {
1132
+ this._modelMountQueued.delete(ms);
1133
+ // 槽已退场(换项目 / 删除):待挂那棵由 `ModelSlot.dispose` → `_detachModel` 当场释放,这里不挂
1134
+ if (!this.models.includes(ms))
1135
+ return;
1136
+ if (ms.mountStaged() && onEachLoad)
1137
+ onEachLoad();
1138
+ }
1096
1139
  /**
1097
- * 每帧把各颗平面那 4 张瓦片的纹理直采绑到材质(⭐ 10-22 晚旨:不走拼合器 RT)。
1098
- * 库里的**唯一驱动点**:持有渲染循环的那一方(编辑态 / 预览 / 下游)每帧调一次,⛔ 不许在调用方再抄一份循环。
1099
- * 还没下载完/缺格的那一格绑 null ⇒ 片元画缺图灰(皇上 10-26 收回默认图那条旨:「不要默认图, 没有加到位的时候 就显示深灰色」)。
1140
+ * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
1141
+ * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
1142
+ * 真正写 uniform 的那一步交给 `threeUtils.addWork` —— 全局 rAF 每拍只跑一项 ⇒ 一帧只绑一颗平面,
1143
+ * 一整层平面的纹理上传被摊到多帧,不再挤在同一记调用里(旧写法一记 `feed` 就把所有平面全绑完)。
1144
+ * ⛔ 不进 `onBeforeRender` 自驱;持有渲染循环的那一方(编辑态 / 预览 / 下游)每帧调一次只是「补排漏网的平面」,
1145
+ * 而整层重建(`setImagePlanes`)末尾自己起排 ⇒ 只调 `importData(整串)` 的宿主不必自己驱动也会长出来。
1146
+ * 还没下载完/缺格的那一格绑 null ⇒ 片元那一格整格 discard,不落像素(用户 10-26 连收两条:先「不要默认图」,
1147
+ * 再「不是显示灰色, 而是直接 discard 丢弃」)。
1100
1148
  */
1101
1149
  feedImagePlaneTiles() {
1102
- if (!this.imagePlanes.length)
1150
+ for (const ip of this.imagePlanes)
1151
+ this._queueImagePlaneBind(ip);
1152
+ }
1153
+ /** 一颗平面排一帧的绑定工作:先把它那 4 条地址**一次性全部**交给下载器,再把「写材质」排进全局队列。 */
1154
+ _queueImagePlaneBind(ip) {
1155
+ const tiles = ip.tiles;
1156
+ const sig = (tiles ?? []).join("|");
1157
+ // 两道闸:已排在队里 = 不重复排;这串地址已经落定过 = 不再动它(换图会改这串 ⇒ 自然会重排)
1158
+ if (this._imgBindQueued.has(ip) || this._imgBindDone.get(ip) === sig)
1103
1159
  return;
1104
1160
  const zero = new Vector4(0, 0, 1, 1);
1105
- for (const ip of this.imagePlanes) {
1106
- const tiles = ip.tiles;
1107
- // ⛔ 旧存档 16 条地址(> 满批 4)直接不收 ⇒ 整张缺图灰,按 10-20 口径重传即可;空 tiles 同样
1108
- if (!tiles || tiles.length !== 4) {
1109
- ip.setTileTextures([null, null, null, null], [zero.clone(), zero.clone(), zero.clone(), zero.clone()]);
1161
+ // ⛔ 旧存档 16 条地址(> 满批 4)直接不收 ⇒ 整张不落像素(四格全 discard),按 10-20 口径重传即可;空 tiles 同样
1162
+ if (!tiles || tiles.length !== 4) {
1163
+ ip.setTileTextures([null, null, null, null], [zero, zero.clone(), zero.clone(), zero.clone()]);
1164
+ this._imgBindDone.set(ip, sig);
1165
+ return;
1166
+ }
1167
+ for (const url of tiles)
1168
+ this._imgTileLoader.preload(url); // ① 一次性全部拿去加载
1169
+ this._imgBindQueued.add(ip);
1170
+ threeUtils.addWork(() => { }); // 占一格(什么都不干),把下面那一项推到下一个 tick —— 与全景上传同一手法
1171
+ threeUtils.addWork(() => this._bindImagePlaneStep(ip, sig));
1172
+ }
1173
+ /** 队列里的一格:只绑**这一颗**平面里已就绪的那几格;还有在途的就重新排队,⛔ 不在这里等、也⛔ 不顺手把别的平面一起做完。 */
1174
+ _bindImagePlaneStep(ip, sig) {
1175
+ this._imgBindQueued.delete(ip);
1176
+ if (!this.imagePlanes.includes(ip))
1177
+ return; // 已摘掉(删平面 / 切项目整层重建)⇒ 这一格什么都不做
1178
+ const tiles = ip.tiles;
1179
+ if ((tiles ?? []).join("|") !== sig)
1180
+ return; // 地址中途换了 ⇒ 下一记 feed 按新拼串重排
1181
+ const zero = new Vector4(0, 0, 1, 1);
1182
+ const texs = [null, null, null, null];
1183
+ const insets = [zero.clone(), zero.clone(), zero.clone(), zero.clone()];
1184
+ let pending = false;
1185
+ for (let i = 0; i < 4; i++) {
1186
+ const url = tiles[i];
1187
+ if (!url)
1110
1188
  continue;
1111
- }
1112
- const texs = [null, null, null, null];
1113
- const insets = [zero.clone(), zero.clone(), zero.clone(), zero.clone()];
1114
- for (let i = 0; i < 4; i++) {
1115
- const url = tiles[i];
1116
- if (!url)
1117
- continue;
1118
- this._imgTileLoader.preload(url);
1119
- const e = this._imgTileLoader.get(url);
1120
- if (!e || !e.tex)
1121
- continue;
1189
+ const e = this._imgTileLoader.get(url);
1190
+ if (e && e.tex) {
1122
1191
  texs[i] = e.tex;
1123
1192
  insets[i] = this._imgTileLoader.inset(e, i);
1124
1193
  }
1125
- ip.setTileTextures(texs, insets);
1194
+ else if (this._imgTileLoader.isPending(url)) {
1195
+ pending = true; // 还在下载/解码 ⇒ 这一格先绑已到的,剩下的下一拍再看
1196
+ }
1197
+ }
1198
+ ip.setTileTextures(texs, insets);
1199
+ if (pending) {
1200
+ this._queueImagePlaneBind(ip);
1201
+ return;
1126
1202
  }
1203
+ this._imgBindDone.set(ip, sig); // 全部落定(含下载失败那格:它留 null,那一格就永久 discard)
1127
1204
  }
1128
1205
  /** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
1129
1206
  * @param projName 项目名(可选,优先于 `setProjectName` 注入的那个)——图片地址落成 `{项目名}/xxx.jpg` */
@@ -1230,7 +1307,7 @@ export default class MktMesh extends Mesh {
1230
1307
  this._uploadQueued.clear(); // 排队项即便回调仍触发,也会因 `!this._uploadQueued.delete(tex)` 早退
1231
1308
  this._ossSizeDepKeys.clear();
1232
1309
  // ⭐ 10-26 旨入库的两层:子对象逐个 dispose,平面那 4 张瓦片的缓存**只在这里**整批清
1233
- // (与上面 `_texCache` 同一条旨 ⇒ 切工程不重建这一份、也就不会闪缺图灰)。
1310
+ // (与上面 `_texCache` 同一条旨 ⇒ 切工程不重建这一份、也就不会再空出一格)。
1234
1311
  for (const ip of this.imagePlanes) {
1235
1312
  this.remove(ip);
1236
1313
  ip.dispose();
@@ -1242,6 +1319,9 @@ export default class MktMesh extends Mesh {
1242
1319
  }
1243
1320
  this.models = [];
1244
1321
  this._imgTileLoader.clear();
1322
+ this._imgBindQueued.clear();
1323
+ this._imgBindDone.clear();
1324
+ this._modelMountQueued.clear();
1245
1325
  this._stitcher.dispose();
1246
1326
  this._glRenderer = null;
1247
1327
  if (this._placeholder)
@@ -213,9 +213,23 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
213
213
  models: ModelSlot[] = [];
214
214
  /**
215
215
  * 那 4 张瓦片的下载/解码缓存。⭐ 与全景 `_texCache` 同一条旨(10-02「纹理都缓存在对象里面,执行 dispose 时才销毁」)
216
- * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不闪缺图灰;只随本对象 `dispose()` 释放。
216
+ * ⇒ 切工程**不**清它,回来那一圈直接命中缓存、不会再空一格;只随本对象 `dispose()` 释放。
217
217
  */
218
218
  private _imgTileLoader = new ImagePlaneTileLoader();
219
+ /**
220
+ * 逐帧绑定用的两张账(2026-10-26 定的方案:地址一次性全部丢给下载,绑材质改成「一帧一颗平面」)。
221
+ * 全局 rAF 每拍只跑一项 `addWork`,而 `addWork` 按闭包引用判重 ⇒ 每次现造的新闭包它判不出来,
222
+ * ⛔ 去重闸门必须像全景上传那张 `_uploadQueued` 一样自备。
223
+ */
224
+ private _imgBindQueued = new Set<ImagePlane>();
225
+ /** 平面 → 「它 4 条地址全部落定(就绪或失败)时的那串地址」⇒ 地址不换就不再重排;上传换图会改这串 ⇒ 自动重排 */
226
+ private _imgBindDone = new Map<ImagePlane, string>();
227
+ /**
228
+ * 模型槽「GLB 已取回、等这一帧把它挂进父元素」的在册表 —— 与上面图片绑定同一手法(用户 10-26 需求:
229
+ * 一次性全部拿去加载,用 `threeUtils.addWork` 逐帧加到场景里)。整层十几个槽同时到货时,
230
+ * 一帧只有一颗进 `_contentRoot`(进图那一拍要重算包围盒 + 排布 + 首次把几何送上 GPU)。
231
+ */
232
+ private _modelMountQueued = new Set<ModelSlot>();
219
233
 
220
234
  constructor(opts: { wallHeight?: number; wallClosed?: boolean } = {}) {
221
235
  // 发布/预览态墙:**固定**用「全景序号直取」着色器(皇上旨 2026-09-29:mktmesh 就只有按照 attribute
@@ -1057,10 +1071,11 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1057
1071
 
1058
1072
  // ───────────── 平面图 / 模型两层(⭐ 皇上旨 2026-10-26「把图片平面和模型一起搬进库里」) ─────────────
1059
1073
 
1060
- /** 单颗图片平面入层(编辑态双击落点用):进数组 + 挂进本对象子树 */
1074
+ /** 单颗图片平面入层(编辑态双击落点用):进数组 + 挂进本对象子树 + 起排它自己的逐帧绑定 */
1061
1075
  addImagePlane(ip: ImagePlane): ImagePlane {
1062
1076
  this.imagePlanes.push(ip);
1063
1077
  this.add(ip);
1078
+ this._queueImagePlaneBind(ip);
1064
1079
  return ip;
1065
1080
  }
1066
1081
 
@@ -1068,6 +1083,8 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1068
1083
  removeImagePlane(ip: ImagePlane): void {
1069
1084
  const i = this.imagePlanes.indexOf(ip);
1070
1085
  if (i >= 0) this.imagePlanes.splice(i, 1);
1086
+ this._imgBindQueued.delete(ip);
1087
+ this._imgBindDone.delete(ip);
1071
1088
  this.remove(ip);
1072
1089
  ip.dispose();
1073
1090
  }
@@ -1079,10 +1096,11 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1079
1096
  return ms;
1080
1097
  }
1081
1098
 
1082
- /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾) */
1099
+ /** 摘掉一槽并释放(子树里的几何/材质/贴图由 `ModelSlot.dispose` 自己收尾;排在后头的挂载帧靠「不在册」早退) */
1083
1100
  removeModel(ms: ModelSlot): void {
1084
1101
  const i = this.models.indexOf(ms);
1085
1102
  if (i >= 0) this.models.splice(i, 1);
1103
+ this._modelMountQueued.delete(ms);
1086
1104
  this.remove(ms);
1087
1105
  ms.dispose();
1088
1106
  }
@@ -1093,6 +1111,8 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1093
1111
  * ⛔ 本方法里不许再补一遍,也⛔ 不许在调用方各自补。
1094
1112
  */
1095
1113
  setImagePlanes(datas: ImagePlaneData[]): void {
1114
+ this._imgBindQueued.clear();
1115
+ this._imgBindDone.clear();
1096
1116
  for (const ip of this.imagePlanes) {
1097
1117
  this.remove(ip);
1098
1118
  ip.dispose();
@@ -1104,14 +1124,20 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1104
1124
  this.imagePlanes.push(ip);
1105
1125
  this.add(ip);
1106
1126
  }
1127
+ // 整层重建完立刻起排:地址一次性全交给下载、绑材质逐帧一记 ⇒ 宿主不驱动这一层也长得出来
1128
+ this.feedImagePlaneTiles();
1107
1129
  }
1108
1130
 
1109
1131
  /**
1110
1132
  * 整层重建模型槽(`importData` 与预览那一路),地址口径的要求同 `setImagePlanes`。
1111
1133
  * ⚠️ GLB 逐只**异步取、不 await**(十几只串一条队会把载入卡死),每只落地那一刻各回调一次 `onEachLoad`:
1112
1134
  * 预览每帧真画 ⇒ 不传;编辑器是按需渲染 ⇒ 传一个「标脏」的钩子,否则模型挂上来屏幕不动。
1135
+ * 2026-10-26 决定:取回与「挂进父元素」分两拍 —— 地址一次性全交给下载(网络 + 解析不占渲染帧),
1136
+ * 真正进场景图那一步交给 `threeUtils.addWork` ⇒ 全局 rAF 每拍只跑一项 = **一帧只挂一颗槽**
1137
+ * (挂载那一拍要重算包围盒 + 排布 + 首次把整棵子树的几何送上 GPU,十几颗挤在同一帧就是他要拆掉的那一卡)。
1113
1138
  */
1114
1139
  setModels(datas: ModelSlotData[], onEachLoad?: () => void): void {
1140
+ this._modelMountQueued.clear();
1115
1141
  for (const ms of this.models) {
1116
1142
  this.remove(ms);
1117
1143
  ms.dispose();
@@ -1121,39 +1147,87 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1121
1147
  const ms = new ModelSlot(d.name);
1122
1148
  ms.applyData(d);
1123
1149
  this.models.push(ms);
1124
- this.add(ms);
1125
- if (d.url) void ms.setModelUrl(d.url).then(() => onEachLoad && onEachLoad());
1150
+ this.add(ms); // 槽本体(占位小立方体)当场进图 ⇒ GLB 在路上时这一槽不凭空消失
1151
+ if (d.url) void ms.loadModel(d.url).then(() => this._queueModelMount(ms, onEachLoad));
1126
1152
  }
1127
1153
  }
1128
1154
 
1155
+ /** 把「备好待挂」的那一槽交给全局队列;⛔ 去重闸门自备(`addWork` 按闭包引用判重,新闭包它判不出来) */
1156
+ private _queueModelMount(ms: ModelSlot, onEachLoad?: () => void): void {
1157
+ if (this._modelMountQueued.has(ms)) return;
1158
+ this._modelMountQueued.add(ms);
1159
+ threeUtils.addWork(() => {});
1160
+ threeUtils.addWork(() => this._mountModelStep(ms, onEachLoad));
1161
+ }
1162
+
1163
+ /** 全局队列里的那一帧:只挂这一槽,挂好才回调(`onEachLoad` = 按需渲染宿主的标脏钩子) */
1164
+ private _mountModelStep(ms: ModelSlot, onEachLoad?: () => void): void {
1165
+ this._modelMountQueued.delete(ms);
1166
+ // 槽已退场(换项目 / 删除):待挂那棵由 `ModelSlot.dispose` → `_detachModel` 当场释放,这里不挂
1167
+ if (!this.models.includes(ms)) return;
1168
+ if (ms.mountStaged() && onEachLoad) onEachLoad();
1169
+ }
1170
+
1129
1171
  /**
1130
- * 每帧把各颗平面那 4 张瓦片的纹理直采绑到材质(⭐ 10-22 晚旨:不走拼合器 RT)。
1131
- * 库里的**唯一驱动点**:持有渲染循环的那一方(编辑态 / 预览 / 下游)每帧调一次,⛔ 不许在调用方再抄一份循环。
1132
- * 还没下载完/缺格的那一格绑 null ⇒ 片元画缺图灰(皇上 10-26 收回默认图那条旨:「不要默认图, 没有加到位的时候 就显示深灰色」)。
1172
+ * 把各颗平面那 4 张瓦片的纹理绑进材质(10-22 晚定的口径:不走拼合器 RT)。
1173
+ * 2026-10-26 决定改成**逐帧**:一次把整层地址全丢给下载器(下载/解码在网络与工作线程侧,不占这一帧),
1174
+ * 真正写 uniform 的那一步交给 `threeUtils.addWork` —— 全局 rAF 每拍只跑一项 ⇒ 一帧只绑一颗平面,
1175
+ * 一整层平面的纹理上传被摊到多帧,不再挤在同一记调用里(旧写法一记 `feed` 就把所有平面全绑完)。
1176
+ * ⛔ 不进 `onBeforeRender` 自驱;持有渲染循环的那一方(编辑态 / 预览 / 下游)每帧调一次只是「补排漏网的平面」,
1177
+ * 而整层重建(`setImagePlanes`)末尾自己起排 ⇒ 只调 `importData(整串)` 的宿主不必自己驱动也会长出来。
1178
+ * 还没下载完/缺格的那一格绑 null ⇒ 片元那一格整格 discard,不落像素(用户 10-26 连收两条:先「不要默认图」,
1179
+ * 再「不是显示灰色, 而是直接 discard 丢弃」)。
1133
1180
  */
1134
1181
  feedImagePlaneTiles(): void {
1135
- if (!this.imagePlanes.length) return;
1182
+ for (const ip of this.imagePlanes) this._queueImagePlaneBind(ip);
1183
+ }
1184
+
1185
+ /** 一颗平面排一帧的绑定工作:先把它那 4 条地址**一次性全部**交给下载器,再把「写材质」排进全局队列。 */
1186
+ private _queueImagePlaneBind(ip: ImagePlane): void {
1187
+ const tiles = ip.tiles;
1188
+ const sig = (tiles ?? []).join("|");
1189
+ // 两道闸:已排在队里 = 不重复排;这串地址已经落定过 = 不再动它(换图会改这串 ⇒ 自然会重排)
1190
+ if (this._imgBindQueued.has(ip) || this._imgBindDone.get(ip) === sig) return;
1136
1191
  const zero = new Vector4(0, 0, 1, 1);
1137
- for (const ip of this.imagePlanes) {
1138
- const tiles = ip.tiles;
1139
- // ⛔ 旧存档 16 条地址(> 满批 4)直接不收 ⇒ 整张缺图灰,按 10-20 口径重传即可;空 tiles 同样
1140
- if (!tiles || tiles.length !== 4) {
1141
- ip.setTileTextures([null, null, null, null], [zero.clone(), zero.clone(), zero.clone(), zero.clone()]);
1142
- continue;
1143
- }
1144
- const texs: (Texture | null)[] = [null, null, null, null];
1145
- const insets: Vector4[] = [zero.clone(), zero.clone(), zero.clone(), zero.clone()];
1146
- for (let i = 0; i < 4; i++) {
1147
- const url = tiles[i];
1148
- if (!url) continue;
1149
- this._imgTileLoader.preload(url);
1150
- const e = this._imgTileLoader.get(url);
1151
- if (!e || !e.tex) continue;
1192
+ // ⛔ 旧存档 16 条地址(> 满批 4)直接不收 ⇒ 整张不落像素(四格全 discard),按 10-20 口径重传即可;空 tiles 同样
1193
+ if (!tiles || tiles.length !== 4) {
1194
+ ip.setTileTextures([null, null, null, null], [zero, zero.clone(), zero.clone(), zero.clone()]);
1195
+ this._imgBindDone.set(ip, sig);
1196
+ return;
1197
+ }
1198
+ for (const url of tiles) this._imgTileLoader.preload(url); // ① 一次性全部拿去加载
1199
+ this._imgBindQueued.add(ip);
1200
+ threeUtils.addWork(() => {}); // 占一格(什么都不干),把下面那一项推到下一个 tick —— 与全景上传同一手法
1201
+ threeUtils.addWork(() => this._bindImagePlaneStep(ip, sig));
1202
+ }
1203
+
1204
+ /** 队列里的一格:只绑**这一颗**平面里已就绪的那几格;还有在途的就重新排队,⛔ 不在这里等、也⛔ 不顺手把别的平面一起做完。 */
1205
+ private _bindImagePlaneStep(ip: ImagePlane, sig: string): void {
1206
+ this._imgBindQueued.delete(ip);
1207
+ if (!this.imagePlanes.includes(ip)) return; // 已摘掉(删平面 / 切项目整层重建)⇒ 这一格什么都不做
1208
+ const tiles = ip.tiles;
1209
+ if ((tiles ?? []).join("|") !== sig) return; // 地址中途换了 ⇒ 下一记 feed 按新拼串重排
1210
+ const zero = new Vector4(0, 0, 1, 1);
1211
+ const texs: (Texture | null)[] = [null, null, null, null];
1212
+ const insets: Vector4[] = [zero.clone(), zero.clone(), zero.clone(), zero.clone()];
1213
+ let pending = false;
1214
+ for (let i = 0; i < 4; i++) {
1215
+ const url = tiles[i];
1216
+ if (!url) continue;
1217
+ const e = this._imgTileLoader.get(url);
1218
+ if (e && e.tex) {
1152
1219
  texs[i] = e.tex;
1153
1220
  insets[i] = this._imgTileLoader.inset(e, i);
1221
+ } else if (this._imgTileLoader.isPending(url)) {
1222
+ pending = true; // 还在下载/解码 ⇒ 这一格先绑已到的,剩下的下一拍再看
1154
1223
  }
1155
- ip.setTileTextures(texs, insets);
1156
1224
  }
1225
+ ip.setTileTextures(texs, insets);
1226
+ if (pending) {
1227
+ this._queueImagePlaneBind(ip);
1228
+ return;
1229
+ }
1230
+ this._imgBindDone.set(ip, sig); // 全部落定(含下载失败那格:它留 null,那一格就永久 discard)
1157
1231
  }
1158
1232
 
1159
1233
  /** 导出数据:墙点位 + 墙高 + 是否闭合 + 纹理数组(url + 矩阵)
@@ -1269,7 +1343,7 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1269
1343
  this._uploadQueued.clear(); // 排队项即便回调仍触发,也会因 `!this._uploadQueued.delete(tex)` 早退
1270
1344
  this._ossSizeDepKeys.clear();
1271
1345
  // ⭐ 10-26 旨入库的两层:子对象逐个 dispose,平面那 4 张瓦片的缓存**只在这里**整批清
1272
- // (与上面 `_texCache` 同一条旨 ⇒ 切工程不重建这一份、也就不会闪缺图灰)。
1346
+ // (与上面 `_texCache` 同一条旨 ⇒ 切工程不重建这一份、也就不会再空出一格)。
1273
1347
  for (const ip of this.imagePlanes) {
1274
1348
  this.remove(ip);
1275
1349
  ip.dispose();
@@ -1281,6 +1355,9 @@ export default class MktMesh extends Mesh<BufferGeometry, ShaderMaterial> {
1281
1355
  }
1282
1356
  this.models = [];
1283
1357
  this._imgTileLoader.clear();
1358
+ this._imgBindQueued.clear();
1359
+ this._imgBindDone.clear();
1360
+ this._modelMountQueued.clear();
1284
1361
  this._stitcher.dispose();
1285
1362
  this._glRenderer = null;
1286
1363
  if (this._placeholder) this._placeholder.dispose();
@@ -46,8 +46,11 @@ export declare class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
46
46
  get url(): string;
47
47
  get naturalW(): number | undefined;
48
48
  get naturalH(): number | undefined;
49
- /** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 显示灰)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
49
+ /** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 那一片整格 discard,不落像素)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
50
50
  setTileTextures(texs: (Texture | null)[], insets: Vector4[]): void;
51
+ /** 只读「此刻材质里绑了什么」的戳记:4 格就绪位 + 已绑纹理的版本号。
52
+ * 按需渲染的宿主拿它比对画面:逐帧绑定只写 uniform 值,⛔ 不动 `material.version`、也不动几何 ⇒ 场景指纹看不见这一变。 */
53
+ tilesBindingStamp(): string;
51
54
  highlight(): void;
52
55
  restoreColor(): void;
53
56
  toData(): ImagePlaneData;
@@ -11,8 +11,9 @@
11
11
  * - 几何 PlaneGeometry(width, height),原点在平面中心;position.y 由调用者设定(通常 height/2 让底部落地)。
12
12
  * - ⭐ 2026-10-22 晚旨:纹理**不走拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,片元按 UV 象限选片 +
13
13
  * 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。材质用 onBeforeCompile 打补丁,
14
- * 注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰、逐张就绪逐张显影。
15
- * (⭐ 皇上 10-26 一度要默认图,同日收回:「不要默认图, 没有加到位的时候 就显示深灰色」⇒ 默认图那整条已删。)
14
+ * 注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片整格 `discard`(一片像素都不落),逐张就绪逐张显影。
15
+ * (⭐ 2026-10-26 用户先要默认图、当晚收回改要灰,同一晚再收:「平面图的图片还没有加载出来的时候, 不是显示灰色,
16
+ * 而是直接 discrad 丢弃」⇒ 默认图与缺图灰两条都已删,没就绪就是透明没有。)
16
17
  * - UV 标准 [0,1]²(PlaneGeometry 自带),4 张瓦片按 2×2 象限铺满 ⇒ 不需要投影 shader,也不需要离屏 RT。
17
18
  */
18
19
  import { Color, DataTexture, FrontSide, Mesh, MeshBasicMaterial, PlaneGeometry, RGBAFormat, Vector4, } from "three";
@@ -36,21 +37,25 @@ const ZERO_INSET = new Vector4(0, 0, 1, 1);
36
37
  */
37
38
  export const IMAGE_PLANE_EDGE_CLIP_UV = 0.004;
38
39
  const EDGE_MAX_UV = 1 - IMAGE_PLANE_EDGE_CLIP_UV;
39
- /** 片元打补丁:4 张瓦片按 UV 象限选片,缺图那片显示灰(与旧缺失灰同一观感)。索引用字面量 0..3 ⇒ GLSL ES 1.00 合法。 */
40
+ /** 片元打补丁:4 张瓦片按 UV 象限选片;⭐ 2026-10-26 用户决定「图片还没有加载出来的时候, 不是显示灰色, 而是直接 discrad 丢弃」
41
+ * ⇒ 这一格没就绪就 `discard`(一片像素都不落,透过去看到的是平面背后的东西),⛔ 不铺灰、⛔ 不画默认图。
42
+ * 索引用字面量 0..3 ⇒ GLSL ES 1.00 合法。 */
40
43
  const IMG_PLANE_MAP_FRAG = `
41
44
  if ( vUv.x < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.x > ${EDGE_MAX_UV} || vUv.y < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.y > ${EDGE_MAX_UV} ) discard;
42
- vec4 imgTexel = vec4(0.8, 0.8, 0.8, 1.0); // 缺图灰
45
+ vec4 imgTexel;
46
+ bool ready = false;
43
47
  vec2 cell = floor(vUv * 2.0);
44
48
  vec2 local = fract(vUv * 2.0);
45
49
  if (cell.y >= 0.5 && cell.x < 0.5) {
46
- if (uValid[0] > 0.5) { vec4 ins = uInset[0]; imgTexel = texture2D(uTiles[0], mix(ins.xy, ins.zw, local)); }
50
+ if (uValid[0] > 0.5) { vec4 ins = uInset[0]; imgTexel = texture2D(uTiles[0], mix(ins.xy, ins.zw, local)); ready = true; }
47
51
  } else if (cell.y >= 0.5 && cell.x >= 0.5) {
48
- if (uValid[1] > 0.5) { vec4 ins = uInset[1]; imgTexel = texture2D(uTiles[1], mix(ins.xy, ins.zw, local)); }
52
+ if (uValid[1] > 0.5) { vec4 ins = uInset[1]; imgTexel = texture2D(uTiles[1], mix(ins.xy, ins.zw, local)); ready = true; }
49
53
  } else if (cell.y < 0.5 && cell.x < 0.5) {
50
- if (uValid[2] > 0.5) { vec4 ins = uInset[2]; imgTexel = texture2D(uTiles[2], mix(ins.xy, ins.zw, local)); }
54
+ if (uValid[2] > 0.5) { vec4 ins = uInset[2]; imgTexel = texture2D(uTiles[2], mix(ins.xy, ins.zw, local)); ready = true; }
51
55
  } else {
52
- if (uValid[3] > 0.5) { vec4 ins = uInset[3]; imgTexel = texture2D(uTiles[3], mix(ins.xy, ins.zw, local)); }
56
+ if (uValid[3] > 0.5) { vec4 ins = uInset[3]; imgTexel = texture2D(uTiles[3], mix(ins.xy, ins.zw, local)); ready = true; }
53
57
  }
58
+ if (!ready) discard;
54
59
  diffuseColor *= imgTexel;
55
60
  `;
56
61
  const DEFAULT_WIDTH = 2;
@@ -79,7 +84,7 @@ export class ImagePlane extends Mesh {
79
84
  });
80
85
  // ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
81
86
  // 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
82
- // 材质打补丁(onBeforeCompile)注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰。
87
+ // 材质打补丁(onBeforeCompile)注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片整格 discard。
83
88
  mat.defines = { USE_UV: "" };
84
89
  const uTiles = { value: [PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX] };
85
90
  const uInset = { value: [ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone()] };
@@ -114,7 +119,7 @@ export class ImagePlane extends Mesh {
114
119
  get url() { return this._data.url; }
115
120
  get naturalW() { return this._data.naturalW; }
116
121
  get naturalH() { return this._data.naturalH; }
117
- /** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 显示灰)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
122
+ /** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 那一片整格 discard,不落像素)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
118
123
  setTileTextures(texs, insets) {
119
124
  for (let i = 0; i < 4; i++) {
120
125
  const t = texs[i] || null;
@@ -124,6 +129,16 @@ export class ImagePlane extends Mesh {
124
129
  this._uValid.value[i] = t ? 1 : 0;
125
130
  }
126
131
  }
132
+ /** 只读「此刻材质里绑了什么」的戳记:4 格就绪位 + 已绑纹理的版本号。
133
+ * 按需渲染的宿主拿它比对画面:逐帧绑定只写 uniform 值,⛔ 不动 `material.version`、也不动几何 ⇒ 场景指纹看不见这一变。 */
134
+ tilesBindingStamp() {
135
+ let s = "";
136
+ for (let i = 0; i < 4; i++) {
137
+ const t = this._uTiles.value[i];
138
+ s += this._uValid.value[i] ? `1@${t ? t.version : -1},` : "0,";
139
+ }
140
+ return s;
141
+ }
127
142
  highlight() {
128
143
  this.material.color.set(HIGHLIGHT_COLOR);
129
144
  }