three-base 2.0.172 → 2.0.180

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.
Files changed (127) hide show
  1. package/FPScontrol.d.ts +15 -24
  2. package/FPScontrol.js +63 -87
  3. package/FPScontrol.ts +64 -88
  4. package/Ground1/Ground1.d.ts +4 -2
  5. package/Ground1/Ground1.js +77 -57
  6. package/Ground1/Ground1.ts +77 -57
  7. package/Mirror.js +0 -4
  8. package/Mirror.ts +0 -4
  9. package/MktMesh/MktMesh.d.ts +3 -2
  10. package/MktMesh/MktMesh.js +27 -8
  11. package/MktMesh/MktMesh.ts +24 -8
  12. package/MktMesh/boxLine2.d.ts +42 -0
  13. package/MktMesh/boxLine2.js +185 -0
  14. package/MktMesh/boxLine2.ts +222 -0
  15. package/MktMesh/imagePlane.d.ts +60 -0
  16. package/MktMesh/imagePlane.js +158 -0
  17. package/MktMesh/imagePlane.ts +203 -0
  18. package/MktMesh/imageTileLoader.d.ts +19 -0
  19. package/MktMesh/imageTileLoader.js +55 -0
  20. package/MktMesh/imageTileLoader.ts +64 -0
  21. package/MktMesh/mktMappingShader.d.ts +6 -5
  22. package/MktMesh/mktMappingShader.js +26 -19
  23. package/MktMesh/mktMappingShader.ts +28 -19
  24. package/MktMesh/mktShader.d.ts +127 -58
  25. package/MktMesh/mktShader.js +192 -117
  26. package/MktMesh/mktShader.ts +251 -127
  27. package/MktMesh/modelSlot.d.ts +123 -0
  28. package/MktMesh/modelSlot.js +339 -0
  29. package/MktMesh/modelSlot.ts +383 -0
  30. package/MktMesh/panoSlotRoster.d.ts +2 -2
  31. package/MktMesh/panoSlotRoster.js +9 -7
  32. package/MktMesh/panoSlotRoster.ts +9 -7
  33. package/MktMesh/panoTileStitcher.d.ts +20 -13
  34. package/MktMesh/panoTileStitcher.js +97 -76
  35. package/MktMesh/panoTileStitcher.ts +119 -90
  36. package/PipeMesh/PipeJoint.d.ts +22 -0
  37. package/PipeMesh/PipeJoint.js +33 -0
  38. package/PipeMesh/PipeJoint.ts +38 -0
  39. package/PipeMesh/PipeMesh.d.ts +94 -22
  40. package/PipeMesh/PipeMesh.js +192 -53
  41. package/PipeMesh/PipeMesh.ts +223 -76
  42. package/PipeMesh/PipeStraight.d.ts +19 -0
  43. package/PipeMesh/PipeStraight.js +34 -0
  44. package/PipeMesh/PipeStraight.ts +45 -0
  45. package/PipeMesh/axisSnap.d.ts +52 -0
  46. package/PipeMesh/axisSnap.js +52 -0
  47. package/PipeMesh/axisSnap.ts +93 -0
  48. package/PipeMesh/directRoute.d.ts +85 -0
  49. package/PipeMesh/directRoute.js +126 -0
  50. package/PipeMesh/directRoute.ts +190 -0
  51. package/PipeMesh/index.d.ts +6 -0
  52. package/PipeMesh/index.js +6 -0
  53. package/PipeMesh/index.ts +7 -1
  54. package/PipeMesh/pipeMaterial.d.ts +16 -0
  55. package/PipeMesh/pipeMaterial.js +23 -0
  56. package/PipeMesh/pipeMaterial.ts +26 -0
  57. package/ThreeBase.d.ts +1 -39
  58. package/ThreeBase.js +38 -147
  59. package/ThreeBase.ts +1176 -1302
  60. package/instanced/InstancedBatch.d.ts +14 -1
  61. package/instanced/InstancedBatch.js +53 -3
  62. package/instanced/InstancedBatch.ts +62 -3
  63. package/instanced/InstancedChild.d.ts +24 -0
  64. package/instanced/InstancedChild.js +40 -0
  65. package/instanced/InstancedChild.ts +41 -0
  66. package/instanced/InstancedGroup.d.ts +48 -0
  67. package/instanced/InstancedGroup.js +45 -1
  68. package/instanced/InstancedGroup.ts +85 -0
  69. package/package.json +22 -7
  70. package/pass/debugDepthState.d.ts +16 -8
  71. package/pass/debugDepthState.js +4 -0
  72. package/pass/debugDepthState.ts +27 -9
  73. package/pass/glow_pass.js +11 -94
  74. package/pass/glow_pass.ts +11 -97
  75. package/pass/ssao_pass.d.ts +9 -23
  76. package/pass/ssao_pass.js +41 -128
  77. package/pass/ssao_pass.ts +36 -166
  78. package/pass/stroke_pass.js +3 -0
  79. package/pass/stroke_pass.ts +4 -0
  80. package/shaders/debugDepthCompareShader.d.ts +1 -1
  81. package/shaders/debugDepthCompareShader.js +1 -1
  82. package/shaders/debugDepthCompareShader.ts +1 -1
  83. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  84. package/shaders/debugDepthJudgeShader.js +1 -1
  85. package/shaders/debugDepthJudgeShader.ts +1 -1
  86. package/PipeMesh/PathGeometry.d.ts +0 -44
  87. package/PipeMesh/PathGeometry.js +0 -307
  88. package/PipeMesh/PathGeometry.ts +0 -343
  89. package/PipeMesh/PathPoint.d.ts +0 -17
  90. package/PipeMesh/PathPoint.js +0 -31
  91. package/PipeMesh/PathPoint.ts +0 -36
  92. package/PipeMesh/PathPointList.d.ts +0 -31
  93. package/PipeMesh/PathPointList.js +0 -209
  94. package/PipeMesh/PathPointList.ts +0 -227
  95. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  96. package/PipeMesh/PathTubeGeometry.js +0 -130
  97. package/PipeMesh/PathTubeGeometry.ts +0 -146
  98. package/WaterFlow/FloatingDuck.d.ts +0 -88
  99. package/WaterFlow/FloatingDuck.js +0 -307
  100. package/WaterFlow/FloatingDuck.ts +0 -445
  101. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  102. package/WaterFlow/GPUFluidSimulation.js +0 -785
  103. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  104. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  105. package/WaterFlow/ReservoirSystem.js +0 -621
  106. package/WaterFlow/ReservoirSystem.ts +0 -754
  107. package/WaterFlow/SPHField.d.ts +0 -35
  108. package/WaterFlow/SPHField.js +0 -204
  109. package/WaterFlow/SPHField.ts +0 -228
  110. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  111. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  112. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  113. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  114. package/WaterFlow/ShallowWaterSurface.js +0 -441
  115. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  116. package/WaterFlow/WaterFlow.d.ts +0 -87
  117. package/WaterFlow/WaterFlow.js +0 -171
  118. package/WaterFlow/WaterFlow.ts +0 -219
  119. package/WaterFlow/types.d.ts +0 -161
  120. package/WaterFlow/types.js +0 -1
  121. package/WaterFlow/types.ts +0 -189
  122. package/WaterFlow/waterFlowPass.d.ts +0 -3
  123. package/WaterFlow/waterFlowPass.js +0 -20
  124. package/WaterFlow/waterFlowPass.ts +0 -40
  125. package/shaders/debugExcludedShader.d.ts +0 -15
  126. package/shaders/debugExcludedShader.js +0 -48
  127. package/shaders/debugExcludedShader.ts +0 -50
@@ -0,0 +1,383 @@
1
+ /*
2
+ * @Description: 模型槽元素 —— 皇上旨 2026-10-20「在 mktmesh 里面添加模型,可以在地面和墙面添加模型,
3
+ * 在初始添加的时候,没有模型就显示成一个小立方体,然后选中小立方体的时候,在编辑弹窗里面可以上传 glb 模型」。
4
+ *
5
+ * 设计(每条都是被现实逼出来的,别顺手简化):
6
+ * - 独立 Group,⛔ 不走 `InstancedChild`:合批的键来自**类级** `static geometry/material`,而 GLB 每个模型
7
+ * 自带一份几何与材质 ⇒ 没有可共用的那一份;且 `InstancedChild` 的选中代理在「类没有 static geometry」时直接跳过。
8
+ * - 两级内容:占位小立方体(未上传时可见,皇上要的那个「小立方体」)+ `modelRoot`(GLB 挂这里)。
9
+ * - ⭐ 皇上旨 2026-10-21「上传模型的时候,不要去修改模型的大小」:上传挂上来的 GLB 保持**它自己的原尺寸**落地。
10
+ * 做法 = 载入成功后由上传入口调一次 `useNativeSize()`,把「尺寸 (m,最长边)」那一栏填成量到的原最长边
11
+ * ⇒ 后面那步归一算出的倍率正好是 1(`scaleTo` 吃的是同一份包围盒数字),画面上一比一、⛔ 不再把一只 20 m 的
12
+ * 展柜压成默认的 1 m。要改大小仍然可以在面板里改那一栏(存档/预览那一路**不**调 `useNativeSize`,
13
+ * 否则他存好的尺寸每次重开项目都被抹掉)。
14
+ * - ⚠️ 排布前必须把 GLB 根节点恢复到**载入时的原始 scale/position**(`_baseScale`/`_basePos`)再量再归一:
15
+ * 导出器常在根上带单位换算缩放(cm→m 那类 0.01),过去无条件 `scale.setScalar(1)` 连根上的那份一起抹掉
16
+ * ⇒ 那就等于改了模型大小;且 `_layout()` 每次都要从同一个基准重算,否则第二次量到的是第一次缩放后的盒子。
17
+ * - 高亮:占位方块换色 + ⭐ 皇上旨 2026-10-22「选中设备的时候,设备还需要有一个高亮的框,参考旁边 archives2026
18
+ * 项目」⇒ 那圈框换成 `BoxLine2`(12 条 fat-line 棱,朝相机绿实线、被盒子挡住的红虚线;旧那圈是
19
+ * `LineSegments + LineBasicMaterial`,`linewidth` 在 WebGL 下是死旋钮 ⇒ 恒 1 像素,深色模型上看不见)。
20
+ * ⛔ 不改用户 GLB 的材质:一个 GLB 里多个 mesh 常共用同一份材质实例,
21
+ * 染色会顺着那份材质串到别的模型槽(同一份 glb 被放在两处时更明显 —— 两处一起变黄)。
22
+ * - ⚠️ 高亮框**懒建**(第一次 `highlight()` 才 new):本类编辑器与预览共用,预览那份从不选中 ⇒
23
+ * 一个 `BoxLine2` 是 12 个 fat-line 对象 + 一份代理盒,⛔ 不许让预览为用不上的东西按槽数付出。
24
+ * - `surface` 只决定**占位小立方体**的落位(floor ⇒ 底面贴原点;wall ⇒ 中心对齐原点)和存档里的落点口径。
25
+ * ⛔ 它**不再**决定用户上传模型的对齐 —— 皇上旨 2026-10-22「你先不过多的处理模型,让他在该有的位置」⇒
26
+ * 内容位置补偿整条删除,GLB 挂进来保持它自己载入时的原生 position(偏心导出的模型不再被拽到槽中心,
27
+ * 代价是原点不在底面的 GLB 会按原生悬空/陷地,那种情况由他在面板里改位置/旋转扳正)。
28
+ * - 朝向约定:模型的「正面」取 **+Z**,与 `ImagePlane` 同一条尺 ⇒ 墙面落点 `ry = atan2(n.x, n.z)`。
29
+ * 面板里「旋转(°)」的 Y 可以任意改,约定错了改一个数就能扳回来,不必重传模型。
30
+ */
31
+ import {
32
+ Box3,
33
+ BoxGeometry,
34
+ Color,
35
+ Group,
36
+ Mesh,
37
+ MeshBasicMaterial,
38
+ Object3D,
39
+ Vector3,
40
+ } from "three";
41
+ import threeLoader from "../threeLoader";
42
+ import threeUtils from "../threeUtils";
43
+ import BoxLine2 from "./boxLine2";
44
+
45
+ /** 模型槽的持久化数据(进 exportData / importData / 预览 loadData) */
46
+ export interface ModelSlotData {
47
+ name?: string;
48
+ position: { x: number; y: number; z: number };
49
+ rotation: { x: number; y: number; z: number };
50
+ /** 归一化后模型最长边的实际米数(GLB 按包围盒等比缩到这里;上传时那一档自动填成模型原最长边 ⇒ 一比一) */
51
+ size: number;
52
+ /** glb 地址;空串 = 还没上传 ⇒ 画面显示占位小立方体 */
53
+ url: string;
54
+ /** 落在地面还是墙面(决定占位方块的落位与落点口径;10-22 旨之后**不再**挪动用户上传模型的位置) */
55
+ surface: "floor" | "wall";
56
+ /** 模型原始包围盒尺寸(米,在 GLB 根节点**原姿势**下量,归一之前)—— 面板显示「原模型多大」用 */
57
+ naturalSize?: { x: number; y: number; z: number };
58
+ }
59
+
60
+ /** 占位小立方体的棱长(米)—— 皇上要的是「小立方体」,比「加立方体」那颗 1m 的单位方块小一档 */
61
+ export const MODEL_SLOT_PLACEHOLDER = 0.3;
62
+ /** 默认尺寸档(米,最长边):还没上传模型时占位方块用不上它;上传成功后 `useNativeSize()` 会把它填成模型原最长边 ⇒ 一比一 */
63
+ export const MODEL_SLOT_DEFAULT_SIZE = 1;
64
+ /** 墙面落点沿法线外推的距离(米):模型中心压在墙面上会让一半陷进墙皮,且贴墙的平面片会 z-fighting */
65
+ export const MODEL_SLOT_WALL_OFFSET = 0.02;
66
+
67
+ const PLACEHOLDER_COLOR = "#c8c8c8";
68
+ const HIGHLIGHT_COLOR = "#ffd24a";
69
+ /** 高亮框套在包围盒外的余量倍率(与旧那圈细线同一档,⛔ 不是新旋钮:框压着模型表面会 z-fighting) */
70
+ const BOX_FIT_SCALE = 1.02;
71
+ /** `setSize` 的临时载体(每次贴框都要用 ⇒ 别每帧 new 一份 Vector3) */
72
+ const BOX_FIT_SIZE = new Vector3();
73
+
74
+ /** 释放一棵子树(GLB 载入失败/换模型/删除时):几何、材质、材质上的贴图各 dispose 一次 */
75
+ function disposeSubtree(root: Object3D): void {
76
+ const done = new Set<unknown>();
77
+ root.traverse((o) => {
78
+ const mesh = o as unknown as {
79
+ isMesh?: boolean;
80
+ geometry?: { dispose(): void };
81
+ material?: { dispose(): void } | Array<{ dispose(): void }>;
82
+ };
83
+ if (!mesh.isMesh) return;
84
+ if (mesh.geometry && !done.has(mesh.geometry)) {
85
+ done.add(mesh.geometry);
86
+ mesh.geometry.dispose();
87
+ }
88
+ const mats = Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
89
+ for (const m of mats) {
90
+ if (done.has(m)) continue;
91
+ done.add(m);
92
+ const withMaps = m as unknown as Record<string, unknown>;
93
+ for (const key of ["map", "normalMap", "roughnessMap", "metalnessMap", "emissiveMap", "aoMap", "alphaMap", "bumpMap", "displacementMap"]) {
94
+ const tex = withMaps[key];
95
+ if (tex && typeof (tex as { dispose?: () => void }).dispose === "function" && !done.has(tex)) {
96
+ done.add(tex);
97
+ (tex as { dispose(): void }).dispose();
98
+ }
99
+ }
100
+ m.dispose();
101
+ }
102
+ });
103
+ }
104
+
105
+ export class ModelSlot extends Group {
106
+ /** 拾取路由用标记(与 three 的 `isInstancedMesh` 同款判据):射线命中任意子对象后向上找带这个标记的祖先 */
107
+ readonly isModelSlot = true;
108
+ /** 占位小立方体(未上传模型时可见) */
109
+ private readonly _placeholder: Mesh<BoxGeometry, MeshBasicMaterial>;
110
+ /** GLB 挂载点:内容在这里被等比归一(10-22 旨之后位置保持 GLB 原生,⛔ 不再挪) */
111
+ private readonly _contentRoot: Group;
112
+ /**
113
+ * 选中高亮框(贴合当前内容的包围盒;10-22 旨整搬 archives2026 的 `BoxLine2`)。
114
+ * ⚠️ 懒建 —— null = 这个槽从没被选中过(预览那一路永远是 null);⛔ 不参与射线拾取(见 `pickIgnored`)。
115
+ */
116
+ private _box: BoxLine2 | null = null;
117
+ /** 高亮框该套在哪、套多大(`_layout()` 每次算好存这里;框还没建起来时先记着,`highlight()` 建好再贴) */
118
+ private _boxFit: { center: Vector3; size: Vector3 } | null = null;
119
+ /** 当前 GLB 子树;null = 没有模型(显示占位方块) */
120
+ private _model: Object3D | null = null;
121
+ /**
122
+ * GLB **载入时的原姿势**(根节点自带的那份 scale/position,导出器常在根上带 cm→m 单位换算缩放)。
123
+ * ⚠️ 每次排布都先从这两个值复位再量再归一 ⇒ 「原尺寸」有唯一基准,且第二次 `_layout()` 不会量到
124
+ * 上一次 `scaleTo` 之后的盒子(那是重复缩放的源头)。`_baseFor` 记录这份基准属于哪一棵子树(换模型要重取)。
125
+ */
126
+ private _baseFor: Object3D | null = null;
127
+ private _baseScale = new Vector3(1, 1, 1);
128
+ private _basePos = new Vector3(0, 0, 0);
129
+ private _url = "";
130
+ private _size = MODEL_SLOT_DEFAULT_SIZE;
131
+ private _surface: "floor" | "wall" = "floor";
132
+ private _naturalSize?: { x: number; y: number; z: number };
133
+ /** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
134
+ private _loadToken = 0;
135
+ private _disposed = false;
136
+
137
+ constructor(name?: string) {
138
+ super();
139
+ this.name = name || "模型槽";
140
+ this.userData.kind = "modelslot";
141
+
142
+ this._placeholder = new Mesh(
143
+ new BoxGeometry(MODEL_SLOT_PLACEHOLDER, MODEL_SLOT_PLACEHOLDER, MODEL_SLOT_PLACEHOLDER),
144
+ new MeshBasicMaterial({ color: new Color(PLACEHOLDER_COLOR) }),
145
+ );
146
+ this._placeholder.name = "模型槽占位方块";
147
+ this.add(this._placeholder);
148
+
149
+ this._contentRoot = new Group();
150
+ this._contentRoot.name = "模型槽内容";
151
+ this.add(this._contentRoot);
152
+
153
+ this._layout();
154
+ }
155
+
156
+ get size(): number { return this._size; }
157
+ get url(): string { return this._url; }
158
+ get surface(): "floor" | "wall" { return this._surface; }
159
+ get naturalSize(): { x: number; y: number; z: number } | undefined { return this._naturalSize; }
160
+ /** 是否已经挂上真模型(false = 仍是占位小立方体) */
161
+ get hasModel(): boolean { return this._model !== null; }
162
+
163
+ /** 改尺寸档(米,最长边):立刻重缩内容 */
164
+ setSize(size: number): void {
165
+ this._size = size > 0 ? size : this._size;
166
+ this._layout();
167
+ }
168
+
169
+ /**
170
+ * 把尺寸档填成**模型自己的原最长边** ⇒ 之后那步归一算出的倍率正好是 1 ⇒ 画面上一比一,⛔ 不改模型大小。
171
+ * ⭐ 皇上旨 2026-10-21「上传模型的时候,不要去修改模型的大小」:只给**上传**那一路调(渲染器 `setModelGlb`);
172
+ * 存档导入 / 预览重建那一路**不能**调,否则他在面板里存的尺寸每次重开项目都被抹回原大小。
173
+ * @returns 是否真的改动了(还没挂上模型 ⇒ false,尺寸档保持原样)
174
+ */
175
+ useNativeSize(): boolean {
176
+ const n = this._naturalSize;
177
+ if (!this._model || !n) return false;
178
+ const maxEdge = Math.max(n.x, n.y, n.z);
179
+ if (!(maxEdge > 0)) return false;
180
+ this._size = maxEdge;
181
+ this._layout();
182
+ return true;
183
+ }
184
+
185
+ /** 改落点面(地面/墙面):重摆占位方块的落位 + 重算高亮框(用户上传的模型位置不动,见 10-22 旨) */
186
+ setSurface(surface: "floor" | "wall"): void {
187
+ this._surface = surface;
188
+ this._layout();
189
+ }
190
+
191
+ /**
192
+ * 载入并挂上 GLB(`url` 必须是可直接 fetch 的绝对地址;空串 = 退回占位小立方体)。
193
+ * @returns 是否真的挂上了(地址无效/载入失败/这次结果已作废 ⇒ false)
194
+ */
195
+ async setModelUrl(url: string): Promise<boolean> {
196
+ this._url = url;
197
+ const token = ++this._loadToken;
198
+ this._detachModel();
199
+ // 先立刻退回占位小立方体:GLB 在路上那段窗口(慢网/大模型)⛔ 不许让这一槽凭空消失
200
+ // (取回失败那条支路本来也是停在占位上,两段时间里画面一致)
201
+ this._layout();
202
+ if (!url) return true;
203
+ const obj = await threeLoader.loadModel(url);
204
+ // 迟到回调:这一槽已经改指向别的地址、或已经删除 ⇒ 当场释放,别留一份没人认领的显存
205
+ if (this._disposed || token !== this._loadToken) {
206
+ if (obj) disposeSubtree(obj);
207
+ return false;
208
+ }
209
+ if (!obj) {
210
+ this._layout();
211
+ return false;
212
+ }
213
+ this._model = obj;
214
+ this._contentRoot.add(obj);
215
+ this._layout();
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * 把存档里的数据写进来(位置 / 旋转 / 尺寸 / 落点面 / 地址)。
221
+ * ⚠️ 刻意**不**在这里发起 GLB 载入:载入是异步的,落地那一刻必须由调用方(编辑器导入 / 预览)
222
+ * 显式标脏才能画出来 —— 换成 `modelRoot` 的子对象不改任何材质版本号。
223
+ * 故本方法只把 `url` 记好,取模型由调用方 `setModelUrl(d.url).then(...)` 驱动(⛔ 别在这儿偷偷 void)。
224
+ */
225
+ applyData(d: ModelSlotData): void {
226
+ if (d.name) this.name = d.name;
227
+ this.position.set(d.position.x, d.position.y, d.position.z);
228
+ this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
229
+ this._surface = d.surface === "wall" ? "wall" : "floor";
230
+ this._size = d.size > 0 ? d.size : MODEL_SLOT_DEFAULT_SIZE;
231
+ this._naturalSize = d.naturalSize;
232
+ this._url = d.url || "";
233
+ if (!this._url) this._detachModel();
234
+ this._layout();
235
+ }
236
+
237
+ toData(): ModelSlotData {
238
+ return {
239
+ name: this.name || undefined,
240
+ position: { x: this.position.x, y: this.position.y, z: this.position.z },
241
+ rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
242
+ size: this._size,
243
+ url: this._url,
244
+ surface: this._surface,
245
+ naturalSize: this._naturalSize,
246
+ };
247
+ }
248
+
249
+ highlight(): void {
250
+ this._placeholder.material.color.set(HIGHLIGHT_COLOR);
251
+ this._ensureBox().visible = true;
252
+ }
253
+
254
+ restoreColor(): void {
255
+ this._placeholder.material.color.set(PLACEHOLDER_COLOR);
256
+ if (this._box) this._box.visible = false;
257
+ }
258
+
259
+ dispose(): void {
260
+ this._disposed = true;
261
+ this._loadToken++;
262
+ this._detachModel();
263
+ this._placeholder.geometry.dispose();
264
+ this._placeholder.material.dispose();
265
+ if (this._box) {
266
+ this._box.dispose();
267
+ this.remove(this._box);
268
+ this._box = null;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * 取出(并第一次需要时建起)高亮框:挂进槽、贴上最近一次算好的包围盒。
274
+ * ⚠️ 建好即 `visible = false` —— 调用方拿到对象后自己决定亮不亮(`highlight()` 里点亮)。
275
+ */
276
+ private _ensureBox(): BoxLine2 {
277
+ if (!this._box) {
278
+ const box = new BoxLine2();
279
+ box.name = `${this.name || "模型槽"}-高亮框`;
280
+ box.visible = false;
281
+ this._box = box;
282
+ this.add(box);
283
+ }
284
+ this._applyBoxFit();
285
+ return this._box;
286
+ }
287
+
288
+ /** 把 `_boxFit` 写进框(尺寸 = 包围盒外扩 `BOX_FIT_SCALE`,中心 = 本槽局部系下的盒心) */
289
+ private _applyBoxFit(): void {
290
+ if (!this._box) return;
291
+ if (!this._boxFit) {
292
+ this._box.visible = false;
293
+ return;
294
+ }
295
+ const s = this._boxFit.size;
296
+ // 零厚度那一路钳到 1e-4(薄一片的 GLB 会量出某个轴 = 0,⛔ 不许把线段端点重合到一处)
297
+ BOX_FIT_SIZE.set(Math.max(s.x, 1e-4), Math.max(s.y, 1e-4), Math.max(s.z, 1e-4)).multiplyScalar(BOX_FIT_SCALE);
298
+ this._box.setSize(BOX_FIT_SIZE);
299
+ this._box.position.copy(this._boxFit.center);
300
+ }
301
+
302
+ /** 摘掉 GLB(几何/材质/贴图一起释放,否则换一只模型就漏一份显存) */
303
+ private _detachModel(): void {
304
+ if (!this._model) return;
305
+ this._contentRoot.remove(this._model);
306
+ disposeSubtree(this._model);
307
+ this._model = null;
308
+ }
309
+
310
+ /**
311
+ * 内容排布的唯一出口:GLB 等比归一 + 占位方块显隐 + 高亮框贴合(⛔ 不再挪模型的位置,见 `_layoutNeutral`)。
312
+ * ⚠️ 每次改 size / surface / 模型都要走这里(⛔ 别在别处直接写 `_contentRoot.position`)。
313
+ * ⚠️ 皇上旨 2026-10-21「模型的位置和对应的边框不在一起」:这段曾把 `Box3.setFromObject` 量到的
314
+ * **世界**盒子直接写进子对象的**局部** `position`(内容补偿与框中心都是),于是槽离原点越远偏得越远
315
+ * —— 框比模型多偏一整份槽的位置。归一与套框两件事都只该**相对本槽**算,故量之前先把槽自身的位移与
316
+ * 旋转摘掉,算完原样装回(10-22 旨删掉内容补偿后,这把「摘了再装回」的尺仍要为高亮框留着)。
317
+ */
318
+ private _layout(): void {
319
+ const savedPos = this.position.clone();
320
+ // ⚠️ 存取都走 `rotation`(欧拉),⛔ 别拿 `quaternion` 存再 copy 回来:四元数↔欧拉那趟换算会把
321
+ // 存档里的角度数字漂掉(1.5708 变 1.57079265…,护栏 BV ⑫ 的落盘逐字闭环当场红)。
322
+ const savedRot = this.rotation.clone();
323
+ this.position.set(0, 0, 0);
324
+ this.rotation.set(0, 0, 0);
325
+ this.updateMatrixWorld(true);
326
+ try {
327
+ this._layoutNeutral();
328
+ } finally {
329
+ this.position.copy(savedPos);
330
+ this.rotation.copy(savedRot);
331
+ this.updateMatrixWorld(true);
332
+ }
333
+ }
334
+
335
+ /** 只在「本槽自身位移/旋转已摘除」的世界帧里跑 ⇒ 下面量的每个盒子都等于相对本槽的盒子 */
336
+ private _layoutNeutral(): void {
337
+ const m = this._model;
338
+ if (m) {
339
+ // 复位到 GLB 载入时的原姿势(根上带单位换算缩放的那种导出器,⛔ 不能顺手 setScalar(1) 抹掉它)
340
+ if (this._baseFor !== m) {
341
+ this._baseFor = m;
342
+ this._baseScale.copy(m.scale);
343
+ this._basePos.copy(m.position);
344
+ }
345
+ m.scale.copy(this._baseScale);
346
+ m.position.copy(this._basePos);
347
+ m.updateMatrixWorld(true);
348
+ const box = new Box3().setFromObject(m);
349
+ if (!box.isEmpty()) {
350
+ const s = box.getSize(new Vector3());
351
+ this._naturalSize = { x: s.x, y: s.y, z: s.z };
352
+ }
353
+ // 等比缩到尺寸档 = 原最长边时(上传那一路,`useNativeSize()` 刚把 `_size` 填成它)倍率正好是 1
354
+ // ⇒ 模型保持原大小;他在面板里把那一栏改成别的数才真的缩放(⛔ 不是逐轴拉满)
355
+ threeUtils.scaleTo(m, this._size);
356
+ m.updateMatrixWorld(true);
357
+ // ⛔ 这里过去还会写一份「位置补偿」(皇上旨 2026-10-22「你先不过多的处理模型,让他在该有的位置」⇒ 删掉):
358
+ // 旧那一刀量出内容包围盒后写 `m.position.set(-cx, -cy, -cz)`,把盒中心拽到槽原点(地面还把**底面**贴 y=0、
359
+ // 墙面三轴居中)。导出时偏心摆放的 GLB(物体在原点旁几十米)或原点不在底面的模型,一进来就被整只挪走。
360
+ // 现在 `m.position` 逐字保持上面 `_basePos` 复位回去的那份 = **GLB 载入时的原生位置**,一行都不改。
361
+ }
362
+ this._placeholder.visible = m === null;
363
+ const ph = MODEL_SLOT_PLACEHOLDER;
364
+ this._placeholder.position.set(
365
+ 0,
366
+ this._surface === "wall" ? 0 : ph / 2,
367
+ 0,
368
+ );
369
+ // 选中框贴合当前内容(外面套 `BOX_FIT_SCALE` 倍免得线与模型表面共面闪)
370
+ // ⚠️ 先更矩阵再量包围盒:上面刚 `scaleTo` 改过根上的 scale,`setFromObject` 读的是 matrixWorld,
371
+ // 不先更新就量到「缩放之前」的盒子 ⇒ 选中框整只偏离模型(皇上旨 10-21 那条血案的同一把尺)。
372
+ this.updateMatrixWorld(true);
373
+ const b = new Box3().setFromObject(m ? m : this._placeholder);
374
+ if (b.isEmpty()) {
375
+ this._boxFit = null;
376
+ } else {
377
+ if (!this._boxFit) this._boxFit = { center: new Vector3(), size: new Vector3() };
378
+ b.getCenter(this._boxFit.center);
379
+ b.getSize(this._boxFit.size);
380
+ }
381
+ this._applyBoxFit();
382
+ }
383
+ }
@@ -11,8 +11,8 @@ export interface PanoRosterCand<T> {
11
11
  * 算出下一帧的槽位名册(**不修改传进来的 roster**,返回新数组)。
12
12
  * @param roster 当前名册(0 号槽 = 上一帧的中心),长度可以小于 maxSlots(内部按 undefined 补齐)
13
13
  * @param dists 本帧全部全景点及其到**锚点**的距离(锚点 = 调用方那把尺子的端点;⭐ 10-07 第二十一道旨后 = 控制器中心点),**数组顺序 = 全景列表顺序**(下标就是旨意里说的「几号全景」)
14
- * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-06 晚旨后是 **8**:8 点 × 每个全景
15
- * 左右 2 张 RT = 16 sampler 仍是 GLSL 物理上限。⛔ 本函数不许假设它恒等于 4)
14
+ * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-23 旨后是 **5**:5 点 × 每个全景
15
+ * 左中右 3 张 RT = 15 sampler,GLSL 物理上限仍是 16。⛔ 本函数不许假设它恒等于 4 或 8)
16
16
  * @param margin 抢中心的死区(米,= `PANO_SLOT_SWAP_MARGIN_M`)
17
17
  * @returns 长度恒 = maxSlots 的新名册:**0 号槽 = 中心**,其余槽 = 列表里按序号一层层往外扩的那些
18
18
  * (同一层先取离锚点更近那侧)。⭐ 10-07 旨「尽可能填满」⇒ 越界的序号不占格、外扩继续往有货那侧走,
@@ -13,8 +13,8 @@
13
13
  * ⇒ 收窄上面那句「如果没有目标全景…就留空」的适用边界:**留空只发生在「项目里的全景总数不够一格一人」时**;
14
14
  * 序号越界的那个位置根本不是一张全景 ⇒ 跳过它、继续往还有货的那一侧扩,槽位不许白留给「不存在的序号」。
15
15
  * 同一道旨还要求:新加的全景点**不必等中心换人**才进 shader ⇒ 中心没换人时在册的人照旧一格不动,空格当场补满。
16
- * ⚠️ 上限仍是 `MKT_PANO_MAX_SLOTS`(=8):8 点 × 左右 2 张拼合槽 = 16 个 sampler,那是 GLSL 物理闸
17
- * (皇上机器 maxTextures=16)⇒ 本函数不越那道闸,只是把那 8 格**尽量用满**。
16
+ * ⚠️ 上限仍是 `MKT_PANO_MAX_SLOTS`(=5):5 点 × 左中右 3 张拼合槽 = 15 个 sampler,那是 GLSL 物理闸
17
+ * (皇上机器 maxTextures=16)⇒ 本函数不越那道闸,只是把那 5 格**尽量用满**。
18
18
  * ⭐ 皇上旨 2026-10-07 第二十一道旨(verbatim):「判断使用那些全景球的判断条件, 从距离相机的距离 改成
19
19
  * 判断距离控制器的中心点的距离来判断」⇒ **本函数的尺子另一端换了人**:函数体一个字不改(它只吃调用方算好的
20
20
  * `d`),改的是**调用方**——编辑态与预览的锚点 = 轨道控制器注视点 `controls.target`(画面正中央那个点),
@@ -44,9 +44,11 @@
44
44
  * ⇒ 本函数把 09-27 那半条**作废**:不再取「最近 4 个」,改取「最近那一张的序号邻居」,槽号也不再按距离排。
45
45
  * - 09-26 的「一进一出 + 0.5m 死区」精神保留,但对象从「谁挤掉谁」变成「谁能抢走中心」。
46
46
  *
47
- * ⚠️ 为什么 0 号槽必须是中心:立方体(`FurnitureObject`)那份 `BoxGeometry` **没有** `aPanoIndex` attribute ⇒
48
- * 序号映射 shader 里 `vPanoIndex` 恒 0 ⇒ 立方体显示的就是名册第 0 格那一张(皇上旨 10-02「模型也按距离相机最近的方式
49
- * 设置全景参数」的落点)。中心坐别的格 = 立方体显示错图。
47
+ * ⚠️ 为什么 0 号槽必须是中心:⭐ 10-02 那档口径下没有 `aPanoIndex` 的那份几何**没有**
48
+ * `aPanoIndex` attribute ⇒ 序号映射 shader 里 `vPanoIndex` 恒 0 ⇒ 全体立方体显示的都是名册第 0 格那一张
49
+ * (中心坐别的格 = 立方体显示错图)。⭐ 皇上旨 2026-10-12 之后立方体**逐颗自带序号**
50
+ * (`InstancedChild.panoIndex` → 逐实例 `aPanoIndex`),这条不再是「立方体显示哪张」的依据;
51
+ * 0 号槽 = 中心照旧(它就是离锚点最近那一张,名册其余格按它的列表邻居外扩)。
50
52
  *
51
53
  * ⚠️ 为什么单独成文件:本函数零依赖(不碰 three / MobX / DOM),`test/mktmesh.test.js` 的 **AP 组**会用 esbuild
52
54
  * 把**这份源码原文**转译后真跑一遍(中心/邻居/外扩/越界不占格/死区/删点/幂等/空格回填 各断言一次)。
@@ -64,8 +66,8 @@ export const PANO_SLOT_SWAP_MARGIN_M = 0.5;
64
66
  * 算出下一帧的槽位名册(**不修改传进来的 roster**,返回新数组)。
65
67
  * @param roster 当前名册(0 号槽 = 上一帧的中心),长度可以小于 maxSlots(内部按 undefined 补齐)
66
68
  * @param dists 本帧全部全景点及其到**锚点**的距离(锚点 = 调用方那把尺子的端点;⭐ 10-07 第二十一道旨后 = 控制器中心点),**数组顺序 = 全景列表顺序**(下标就是旨意里说的「几号全景」)
67
- * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-06 晚旨后是 **8**:8 点 × 每个全景
68
- * 左右 2 张 RT = 16 sampler 仍是 GLSL 物理上限。⛔ 本函数不许假设它恒等于 4)
69
+ * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-23 旨后是 **5**:5 点 × 每个全景
70
+ * 左中右 3 张 RT = 15 sampler,GLSL 物理上限仍是 16。⛔ 本函数不许假设它恒等于 4 或 8)
69
71
  * @param margin 抢中心的死区(米,= `PANO_SLOT_SWAP_MARGIN_M`)
70
72
  * @returns 长度恒 = maxSlots 的新名册:**0 号槽 = 中心**,其余槽 = 列表里按序号一层层往外扩的那些
71
73
  * (同一层先取离锚点更近那侧)。⭐ 10-07 旨「尽可能填满」⇒ 越界的序号不占格、外扩继续往有货那侧走,
@@ -13,8 +13,8 @@
13
13
  * ⇒ 收窄上面那句「如果没有目标全景…就留空」的适用边界:**留空只发生在「项目里的全景总数不够一格一人」时**;
14
14
  * 序号越界的那个位置根本不是一张全景 ⇒ 跳过它、继续往还有货的那一侧扩,槽位不许白留给「不存在的序号」。
15
15
  * 同一道旨还要求:新加的全景点**不必等中心换人**才进 shader ⇒ 中心没换人时在册的人照旧一格不动,空格当场补满。
16
- * ⚠️ 上限仍是 `MKT_PANO_MAX_SLOTS`(=8):8 点 × 左右 2 张拼合槽 = 16 个 sampler,那是 GLSL 物理闸
17
- * (皇上机器 maxTextures=16)⇒ 本函数不越那道闸,只是把那 8 格**尽量用满**。
16
+ * ⚠️ 上限仍是 `MKT_PANO_MAX_SLOTS`(=5):5 点 × 左中右 3 张拼合槽 = 15 个 sampler,那是 GLSL 物理闸
17
+ * (皇上机器 maxTextures=16)⇒ 本函数不越那道闸,只是把那 5 格**尽量用满**。
18
18
  * ⭐ 皇上旨 2026-10-07 第二十一道旨(verbatim):「判断使用那些全景球的判断条件, 从距离相机的距离 改成
19
19
  * 判断距离控制器的中心点的距离来判断」⇒ **本函数的尺子另一端换了人**:函数体一个字不改(它只吃调用方算好的
20
20
  * `d`),改的是**调用方**——编辑态与预览的锚点 = 轨道控制器注视点 `controls.target`(画面正中央那个点),
@@ -44,9 +44,11 @@
44
44
  * ⇒ 本函数把 09-27 那半条**作废**:不再取「最近 4 个」,改取「最近那一张的序号邻居」,槽号也不再按距离排。
45
45
  * - 09-26 的「一进一出 + 0.5m 死区」精神保留,但对象从「谁挤掉谁」变成「谁能抢走中心」。
46
46
  *
47
- * ⚠️ 为什么 0 号槽必须是中心:立方体(`FurnitureObject`)那份 `BoxGeometry` **没有** `aPanoIndex` attribute ⇒
48
- * 序号映射 shader 里 `vPanoIndex` 恒 0 ⇒ 立方体显示的就是名册第 0 格那一张(皇上旨 10-02「模型也按距离相机最近的方式
49
- * 设置全景参数」的落点)。中心坐别的格 = 立方体显示错图。
47
+ * ⚠️ 为什么 0 号槽必须是中心:⭐ 10-02 那档口径下没有 `aPanoIndex` 的那份几何**没有**
48
+ * `aPanoIndex` attribute ⇒ 序号映射 shader 里 `vPanoIndex` 恒 0 ⇒ 全体立方体显示的都是名册第 0 格那一张
49
+ * (中心坐别的格 = 立方体显示错图)。⭐ 皇上旨 2026-10-12 之后立方体**逐颗自带序号**
50
+ * (`InstancedChild.panoIndex` → 逐实例 `aPanoIndex`),这条不再是「立方体显示哪张」的依据;
51
+ * 0 号槽 = 中心照旧(它就是离锚点最近那一张,名册其余格按它的列表邻居外扩)。
50
52
  *
51
53
  * ⚠️ 为什么单独成文件:本函数零依赖(不碰 three / MobX / DOM),`test/mktmesh.test.js` 的 **AP 组**会用 esbuild
52
54
  * 把**这份源码原文**转译后真跑一遍(中心/邻居/外扩/越界不占格/死区/删点/幂等/空格回填 各断言一次)。
@@ -72,8 +74,8 @@ export interface PanoRosterCand<T> {
72
74
  * 算出下一帧的槽位名册(**不修改传进来的 roster**,返回新数组)。
73
75
  * @param roster 当前名册(0 号槽 = 上一帧的中心),长度可以小于 maxSlots(内部按 undefined 补齐)
74
76
  * @param dists 本帧全部全景点及其到**锚点**的距离(锚点 = 调用方那把尺子的端点;⭐ 10-07 第二十一道旨后 = 控制器中心点),**数组顺序 = 全景列表顺序**(下标就是旨意里说的「几号全景」)
75
- * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-06 晚旨后是 **8**:8 点 × 每个全景
76
- * 左右 2 张 RT = 16 sampler 仍是 GLSL 物理上限。⛔ 本函数不许假设它恒等于 4)
77
+ * @param maxSlots shader 的槽位上限(= `MKT_PANO_MAX_SLOTS`;⭐ 2026-10-23 旨后是 **5**:5 点 × 每个全景
78
+ * 左中右 3 张 RT = 15 sampler,GLSL 物理上限仍是 16。⛔ 本函数不许假设它恒等于 4 或 8)
77
79
  * @param margin 抢中心的死区(米,= `PANO_SLOT_SWAP_MARGIN_M`)
78
80
  * @returns 长度恒 = maxSlots 的新名册:**0 号槽 = 中心**,其余槽 = 列表里按序号一层层往外扩的那些
79
81
  * (同一层先取离锚点更近那侧)。⭐ 10-07 旨「尽可能填满」⇒ 越界的序号不占格、外扩继续往有货那侧走,
@@ -1,11 +1,14 @@
1
1
  import { Texture, type WebGLRenderer } from "three";
2
+ import { type TileLayout } from "./mktShader";
2
3
  /**
3
4
  * 拼合器对外接口(消费方 `MktMesh` / 编辑器渲染器 / 预览只认这一个类)。
4
5
  * 10-06 之前它是「两份实现共用一份接口」里的 canvas 那一份 ⇒ 那份已奉旨删除,接口留在原地当文档。
5
6
  */
6
7
  export interface PanoStitcher {
7
- /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做) */
8
- ensure(urls: string[]): void;
8
+ /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
9
+ * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 3×3 / 3 槽 / 4096×6144,10-23 旨);
10
+ * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。 */
11
+ ensure(urls: string[], layout?: TileLayout): void;
9
12
  /**
10
13
  * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
11
14
  * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
@@ -55,7 +58,8 @@ export declare class PanoTileStitcher implements PanoStitcher {
55
58
  private _scene;
56
59
  private _camera;
57
60
  private _mat;
58
- /** 几何是 2×2(铺满正交相机的 [-1,1]²),每格只把它缩到 1/4 再挪过去 ⇒ 一格 = 槽的 1/16 */
61
+ /** 几何是 2×2(铺满正交相机的 [-1,1]²),每格按 `1/slotCellCols × 1/slotCellRows` 缩放再挪过去
62
+ * ⇒ 全景一格 = 槽的 1/3(横 1 竖 3,10-23 旨),图片平面一格 = 1/4(横 2 竖 2) */
59
63
  private _quad;
60
64
  /** 缺格那一路 `uMap` 也要有个合法纹理可绑(1×1,从不真采样) */
61
65
  private _blank;
@@ -63,13 +67,13 @@ export declare class PanoTileStitcher implements PanoStitcher {
63
67
  private _rtTextures;
64
68
  constructor();
65
69
  /** 确保批次已建组、加载已发起。幂等:同批已在册 ⇒ 直接返回(一片瓦片终身只下载、只解码一次)。
66
- * ⭐ 09-29 晚旨「不足的涂灰」+ 10-06 晚旨「8×4 = 32 片」:1~32 条皆可入组,内部补空串占位到 32;
70
+ * ⭐ 09-29 晚旨「不足的涂灰」+ 10-23 旨「3×3 = 9 片」:1~9 条皆可入组,内部补空串占位到 9;
67
71
  * 空串 / 死链的格子拼合时涂灰(一眼看出该全景数据不全)。
68
- * ⛔ 旧版 16 条(4×4)与 64 条(8×8)的数据**都不做兼容**(皇上「无须兼容正常使用, 有异常我会重传」):
69
- * 64 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
70
- * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null,null,null]` —— 槽数再变会漏改)。 */
71
- ensure(urls: string[]): void;
72
- /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 16 格画满)。
72
+ * ⛔ 旧版 16 条(4×4)、32 条(8×4)与 64 条(8×8)的数据**都不做兼容**(皇上「无须兼容正常使用, 有异常我会重传」):
73
+ * 超 9 条会在上一行被判超限直接丢弃;真喂进来的短数据只占前几格 + 其余涂灰,那是**故意**让数据不全看得见。
74
+ * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null,null]` —— 槽数再变会漏改)。 */
75
+ ensure(urls: string[], layout?: TileLayout): void;
76
+ /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
73
77
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
74
78
  peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
75
79
  /** ⭐ 这条路交出去的槽纹理本来就是 GL 渲染目标的附件 —— 像素已经在显存里,没有「上传」这一步。
@@ -116,8 +120,9 @@ export declare class PanoTileStitcher implements PanoStitcher {
116
120
  * 否则会对已释放纹理 `initTexture` ⇒ 凭空把显存建回来且无人回收。
117
121
  * (与 `MktMesh._uploadTileTexture` 的「排上队之后已被清掉 ⇒ 本项作废」同一口径。) */
118
122
  private _uploadTile;
119
- /** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(16 格里的一格)。
120
- * ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 16 帧**,
123
+ /** 画一格:把那片瓦片的**中间那块**铺进该槽第 `cell` 格(每槽 `slotCellCols × slotCellRows` 格里的一格,
124
+ * 10-23 旨后全景是「横 1 × 竖 3」= 3 格、图片平面是 2×2 = 4 格)。
125
+ * ⚠️⚠️ `autoClear` 必须关掉:three 每次 `render()` 默认清屏,而这里**同一张 RT 要连画 cellsPerSlot 帧**,
121
126
  * 清一次就把前面画好的格子抹了 ⇒ 槽纹理只剩最后一格。清屏交给我们自己(见 `_clearRt`)。 */
122
127
  private _stitchCell;
123
128
  /** 把整张 RT 涂成缺失灰:RT 建好后的第一件事 ⇒ 保证「画不满也先喂给 shader」时**没有未定义像素**。
@@ -135,8 +140,10 @@ export declare class PanoTileStitcher implements PanoStitcher {
135
140
  /** 换 renderer:RT 是旧上下文的私有资源,留着既画不了也释放不掉 ⇒ 在册组整份作废,
136
141
  * 靠消费方每帧照旧的 `ensure()` 重建(瓦片走浏览器 HTTP 缓存,不重新编码)。 */
137
142
  private _switchRenderer;
138
- /** 懒建某槽的渲染目标:4096×4096 RGBA8 + sRGB + mipmap,并**立刻整张涂缺失灰**
139
- * (⛔ 未定义像素不许进 shader:`peek()` 现在画过 1 格就交出去)。 */
143
+ /** 懒建某槽的渲染目标:尺寸取自 `g.layout.rtW/rtH`(全景档 = **4096×6144**,10-23 旨)、RGBA8 + sRGB + mipmap,
144
+ * 并**立刻整张涂缺失灰**(⛔ 未定义像素不许进 shader:`peek()` 现在画过 1 格就交出去)。
145
+ * ⚠️ 6144 这个数受本机 `MAX_TEXTURE_SIZE` / 最大渲染目标尺寸约束(ANGLE/D3D11 的 11_0 是 16384,远超;
146
+ * 只有老 Intel 集显才可能卡 4096 —— 真建不出来 three 会报 `GL_INVALID_VALUE`,届时墙上整片缺失灰)。 */
140
147
  private _ensureRt;
141
148
  /** 释放一片瓦片的纹理 + ImageBitmap(它只属于一格,画完即弃 ⇒ 显存峰值 = 一格) */
142
149
  private _dropTile;