three-base 2.0.196 → 2.0.198

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.
@@ -16,7 +16,10 @@
16
16
  * 而是直接 discrad 丢弃」⇒ 默认图与缺图灰两条都已删,没就绪就是透明没有。)
17
17
  * - UV 标准 [0,1]²(PlaneGeometry 自带),4 张瓦片按 2×2 象限铺满 ⇒ 不需要投影 shader,也不需要离屏 RT。
18
18
  */
19
- import { Color, DataTexture, FrontSide, Mesh, MeshBasicMaterial, PlaneGeometry, RGBAFormat, Vector4, } from "three";
19
+ import { Color, DataTexture, FrontSide, Mesh, MeshBasicMaterial, PlaneGeometry, RGBAFormat, Vector2, Vector4, } from "three";
20
+ import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
21
+ import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
22
+ import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
20
23
  /*
21
24
  * ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
22
25
  * 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
@@ -61,6 +64,9 @@ const IMG_PLANE_MAP_FRAG = `
61
64
  const DEFAULT_WIDTH = 2;
62
65
  const DEFAULT_HEIGHT = 1.5;
63
66
  const HIGHLIGHT_COLOR = "#ffd24a";
67
+ /** 选中边框:颜色/粗细与模型槽那圈 `BoxLine2` 的朝向相机的棱同一档(0x00ff00 / 2 屏幕像素) */
68
+ const SELECT_LINE_COLOR = 0x00ff00;
69
+ const SELECT_LINE_WIDTH_PX = 2;
64
70
  export class ImagePlane extends Mesh {
65
71
  _originalColor = "#ffffff";
66
72
  _uTiles;
@@ -72,6 +78,10 @@ export class ImagePlane extends Mesh {
72
78
  };
73
79
  /** 内容是否已落定:4 格里至少有一格真的绑上了纹理。**只朝 true 翻一次**(之后逐张显影继续,但不再回退成「没内容」)。 */
74
80
  _loaded = false;
81
+ /** 此刻是不是编辑器的选中态(决定上不上黄、边框亮不亮) */
82
+ _selected = false;
83
+ /** 选中边框:懒建(预览/发布那一路从不选中 ⇒ 一颗都不建) */
84
+ _outline = null;
75
85
  constructor(name) {
76
86
  const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
77
87
  const mat = new MeshBasicMaterial({
@@ -115,6 +125,7 @@ export class ImagePlane extends Mesh {
115
125
  setPlaneSize(w, h) {
116
126
  this.geometry.dispose();
117
127
  this.geometry = new PlaneGeometry(w, h);
128
+ this._layoutOutline(); // 边框跟着宽高走,否则面板改完尺寸框还停在旧那一圈
118
129
  }
119
130
  setPlaneData(d) {
120
131
  this._data = { url: d.url, tiles: d.tiles, naturalW: d.naturalW, naturalH: d.naturalH };
@@ -125,6 +136,7 @@ export class ImagePlane extends Mesh {
125
136
  get naturalH() { return this._data.naturalH; }
126
137
  /** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 那一片整格 discard,不落像素)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
127
138
  setTileTextures(texs, insets) {
139
+ const hadContent = this._loaded;
128
140
  for (let i = 0; i < 4; i++) {
129
141
  const t = texs[i] || null;
130
142
  this._uTiles.value[i] = t || PLACEHOLDER_TEX;
@@ -136,6 +148,10 @@ export class ImagePlane extends Mesh {
136
148
  if (t)
137
149
  this._loaded = true;
138
150
  }
151
+ // 上传前这颗是选中态(黄),第一张图一到就把黄摘掉 ⇒ 否则「有图不上色」只在再次选中时才生效,
152
+ // 而他报的正是「上传后」那一刻。
153
+ if (!hadContent && this._loaded)
154
+ this._syncSelection();
139
155
  }
140
156
  /** 只读「此刻材质里绑了什么」的戳记:4 格就绪位 + 已绑纹理的版本号。
141
157
  * 按需渲染的宿主拿它比对画面:逐帧绑定只写 uniform 值,⛔ 不动 `material.version`、也不动几何 ⇒ 场景指纹看不见这一变。 */
@@ -148,10 +164,66 @@ export class ImagePlane extends Mesh {
148
164
  return s;
149
165
  }
150
166
  highlight() {
151
- this.material.color.set(HIGHLIGHT_COLOR);
167
+ this._selected = true;
168
+ this._syncSelection();
152
169
  }
153
170
  restoreColor() {
154
- this.material.color.set(this._originalColor);
171
+ this._selected = false;
172
+ this._syncSelection();
173
+ }
174
+ /**
175
+ * 选中态的三样东西一起算(颜色 + 边框),一处规则:
176
+ * - 黄只在**还没上传图**那一档:片元里贴图是乘进 diffuse 的(`diffuseColor *= imgTexel`),有图还套黄 ⇒ 整张图发黄。
177
+ * - 边框在有图/没图两档都亮:没图那四格是整格 `discard`(一个像素都不落),只靠黄等于没有反馈,看不见也找不着。
178
+ */
179
+ _syncSelection() {
180
+ this.material.color.set(this._selected && !this._loaded ? HIGHLIGHT_COLOR : this._originalColor);
181
+ if (!this._selected) {
182
+ if (this._outline)
183
+ this._outline.visible = false;
184
+ return;
185
+ }
186
+ this._ensureOutline();
187
+ this._layoutOutline();
188
+ }
189
+ /** 选中边框懒建:预览/发布那一路从不选中 ⇒ 一颗 LineSegments2 都不建(与模型槽那圈 `BoxLine2` 同口径) */
190
+ _ensureOutline() {
191
+ if (this._outline)
192
+ return;
193
+ const material = new LineMaterial({
194
+ color: SELECT_LINE_COLOR,
195
+ worldUnits: false,
196
+ linewidth: SELECT_LINE_WIDTH_PX,
197
+ // ⚠️ `depthTest: false`:这一圈就躺在平面自己那一平面上,留着深度测试必与图打摆(闪烁)
198
+ depthTest: false,
199
+ resolution: new Vector2(1, 1),
200
+ });
201
+ const outline = new LineSegments2(new LineSegmentsGeometry(), material);
202
+ outline.name = `${this.name}-选中边框`;
203
+ outline.renderOrder = 100; // 与 `BoxLine2` 那圈同档:压在图/墙之后
204
+ outline.frustumCulled = false;
205
+ outline.userData.pickIgnored = true;
206
+ outline.raycast = () => { }; // 拾取打平面本体,边框不是可点的东西
207
+ outline.onBeforeRender = (renderer) => {
208
+ renderer.getSize(material.resolution); // 屏幕像素粗细要每帧拿画布尺寸
209
+ };
210
+ this.add(outline);
211
+ this._outline = outline;
212
+ }
213
+ /** 四条边按当前几何的宽高铺在平面那一平面上(局部 z=0,中心为原点) */
214
+ _layoutOutline() {
215
+ if (!this._outline)
216
+ return;
217
+ const w = this.imageWidth / 2;
218
+ const h = this.imageHeight / 2;
219
+ const c = [
220
+ [-w, -h, 0], [w, -h, 0],
221
+ [w, -h, 0], [w, h, 0],
222
+ [w, h, 0], [-w, h, 0],
223
+ [-w, h, 0], [-w, -h, 0],
224
+ ].flat();
225
+ this._outline.geometry.setPositions(c);
226
+ this._outline.visible = true;
155
227
  }
156
228
  toData() {
157
229
  return {
@@ -177,6 +249,10 @@ export class ImagePlane extends Mesh {
177
249
  dispose() {
178
250
  this.geometry.dispose();
179
251
  this.material.dispose();
252
+ if (this._outline) {
253
+ this._outline.geometry.dispose();
254
+ this._outline.material.dispose();
255
+ }
180
256
  }
181
257
  }
182
258
  export const IMAGE_PLANE_DEFAULTS = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
@@ -25,8 +25,13 @@ import {
25
25
  PlaneGeometry,
26
26
  RGBAFormat,
27
27
  Texture,
28
+ Vector2,
28
29
  Vector4,
30
+ WebGLRenderer,
29
31
  } from "three";
32
+ import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
33
+ import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
34
+ import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
30
35
 
31
36
  /*
32
37
  * ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
@@ -92,6 +97,9 @@ export interface ImagePlaneData {
92
97
  const DEFAULT_WIDTH = 2;
93
98
  const DEFAULT_HEIGHT = 1.5;
94
99
  const HIGHLIGHT_COLOR = "#ffd24a";
100
+ /** 选中边框:颜色/粗细与模型槽那圈 `BoxLine2` 的朝向相机的棱同一档(0x00ff00 / 2 屏幕像素) */
101
+ const SELECT_LINE_COLOR = 0x00ff00;
102
+ const SELECT_LINE_WIDTH_PX = 2;
95
103
 
96
104
  export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
97
105
  private _originalColor: string = "#ffffff";
@@ -104,6 +112,10 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
104
112
  };
105
113
  /** 内容是否已落定:4 格里至少有一格真的绑上了纹理。**只朝 true 翻一次**(之后逐张显影继续,但不再回退成「没内容」)。 */
106
114
  private _loaded = false;
115
+ /** 此刻是不是编辑器的选中态(决定上不上黄、边框亮不亮) */
116
+ private _selected = false;
117
+ /** 选中边框:懒建(预览/发布那一路从不选中 ⇒ 一颗都不建) */
118
+ private _outline: LineSegments2 | null = null;
107
119
 
108
120
  constructor(name?: string) {
109
121
  const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
@@ -153,6 +165,7 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
153
165
  setPlaneSize(w: number, h: number): void {
154
166
  this.geometry.dispose();
155
167
  this.geometry = new PlaneGeometry(w, h);
168
+ this._layoutOutline(); // 边框跟着宽高走,否则面板改完尺寸框还停在旧那一圈
156
169
  }
157
170
 
158
171
  setPlaneData(d: Pick<ImagePlaneData, "url" | "tiles" | "naturalW" | "naturalH">): void {
@@ -166,6 +179,7 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
166
179
 
167
180
  /** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 那一片整格 discard,不落像素)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
168
181
  setTileTextures(texs: (Texture | null)[], insets: Vector4[]): void {
182
+ const hadContent = this._loaded;
169
183
  for (let i = 0; i < 4; i++) {
170
184
  const t = texs[i] || null;
171
185
  this._uTiles.value[i] = t || PLACEHOLDER_TEX;
@@ -175,6 +189,9 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
175
189
  // 「逐张就绪逐张显影」是本文件 10-22 晚定下的口径,等到全到齐会让先到的那几格白等几帧)。
176
190
  if (t) this._loaded = true;
177
191
  }
192
+ // 上传前这颗是选中态(黄),第一张图一到就把黄摘掉 ⇒ 否则「有图不上色」只在再次选中时才生效,
193
+ // 而他报的正是「上传后」那一刻。
194
+ if (!hadContent && this._loaded) this._syncSelection();
178
195
  }
179
196
 
180
197
  /** 只读「此刻材质里绑了什么」的戳记:4 格就绪位 + 已绑纹理的版本号。
@@ -189,11 +206,67 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
189
206
  }
190
207
 
191
208
  highlight(): void {
192
- this.material.color.set(HIGHLIGHT_COLOR);
209
+ this._selected = true;
210
+ this._syncSelection();
193
211
  }
194
212
 
195
213
  restoreColor(): void {
196
- this.material.color.set(this._originalColor);
214
+ this._selected = false;
215
+ this._syncSelection();
216
+ }
217
+
218
+ /**
219
+ * 选中态的三样东西一起算(颜色 + 边框),一处规则:
220
+ * - 黄只在**还没上传图**那一档:片元里贴图是乘进 diffuse 的(`diffuseColor *= imgTexel`),有图还套黄 ⇒ 整张图发黄。
221
+ * - 边框在有图/没图两档都亮:没图那四格是整格 `discard`(一个像素都不落),只靠黄等于没有反馈,看不见也找不着。
222
+ */
223
+ private _syncSelection(): void {
224
+ this.material.color.set(this._selected && !this._loaded ? HIGHLIGHT_COLOR : this._originalColor);
225
+ if (!this._selected) {
226
+ if (this._outline) this._outline.visible = false;
227
+ return;
228
+ }
229
+ this._ensureOutline();
230
+ this._layoutOutline();
231
+ }
232
+
233
+ /** 选中边框懒建:预览/发布那一路从不选中 ⇒ 一颗 LineSegments2 都不建(与模型槽那圈 `BoxLine2` 同口径) */
234
+ private _ensureOutline(): void {
235
+ if (this._outline) return;
236
+ const material = new LineMaterial({
237
+ color: SELECT_LINE_COLOR,
238
+ worldUnits: false,
239
+ linewidth: SELECT_LINE_WIDTH_PX,
240
+ // ⚠️ `depthTest: false`:这一圈就躺在平面自己那一平面上,留着深度测试必与图打摆(闪烁)
241
+ depthTest: false,
242
+ resolution: new Vector2(1, 1),
243
+ });
244
+ const outline = new LineSegments2(new LineSegmentsGeometry(), material);
245
+ outline.name = `${this.name}-选中边框`;
246
+ outline.renderOrder = 100; // 与 `BoxLine2` 那圈同档:压在图/墙之后
247
+ outline.frustumCulled = false;
248
+ outline.userData.pickIgnored = true;
249
+ outline.raycast = (): void => { }; // 拾取打平面本体,边框不是可点的东西
250
+ outline.onBeforeRender = (renderer: WebGLRenderer): void => {
251
+ renderer.getSize(material.resolution); // 屏幕像素粗细要每帧拿画布尺寸
252
+ };
253
+ this.add(outline);
254
+ this._outline = outline;
255
+ }
256
+
257
+ /** 四条边按当前几何的宽高铺在平面那一平面上(局部 z=0,中心为原点) */
258
+ private _layoutOutline(): void {
259
+ if (!this._outline) return;
260
+ const w = this.imageWidth / 2;
261
+ const h = this.imageHeight / 2;
262
+ const c = [
263
+ [-w, -h, 0], [w, -h, 0],
264
+ [w, -h, 0], [w, h, 0],
265
+ [w, h, 0], [-w, h, 0],
266
+ [-w, h, 0], [-w, -h, 0],
267
+ ].flat();
268
+ this._outline.geometry.setPositions(c);
269
+ this._outline.visible = true;
197
270
  }
198
271
 
199
272
  toData(): ImagePlaneData {
@@ -221,6 +294,10 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
221
294
  dispose(): void {
222
295
  this.geometry.dispose();
223
296
  this.material.dispose();
297
+ if (this._outline) {
298
+ this._outline.geometry.dispose();
299
+ this._outline.material.dispose();
300
+ }
224
301
  }
225
302
  }
226
303
 
@@ -1,4 +1,6 @@
1
1
  import { Group } from "three";
2
+ /** glb 地址 → 文件名(日志唯一口径:去掉查询串取最后一段;⛔ 别在打印点各写一份 split) */
3
+ export declare function glbFileName(url: string): string;
2
4
  /** 模型槽的持久化数据(进 exportData / importData / 预览 loadData) */
3
5
  export interface ModelSlotData {
4
6
  name?: string;
@@ -32,6 +32,10 @@ import { Box3, BoxGeometry, Color, Group, Mesh, MeshBasicMaterial, Vector3, } fr
32
32
  import threeLoader from "../threeLoader";
33
33
  import threeUtils from "../threeUtils";
34
34
  import BoxLine2 from "./boxLine2";
35
+ /** glb 地址 → 文件名(日志唯一口径:去掉查询串取最后一段;⛔ 别在打印点各写一份 split) */
36
+ export function glbFileName(url) {
37
+ return url.split("?")[0].split("/").pop() || url;
38
+ }
35
39
  /** 占位小立方体的棱长(米)—— 皇上要的是「小立方体」,比「加立方体」那颗 1m 的单位方块小一档 */
36
40
  export const MODEL_SLOT_PLACEHOLDER = 0.3;
37
41
  /** 默认尺寸档(米,最长边):还没上传模型时占位方块用不上它;上传成功后 `useNativeSize()` 会把它填成模型原最长边 ⇒ 一比一 */
@@ -179,6 +183,9 @@ export class ModelSlot extends Group {
179
183
  this._loaded = true;
180
184
  return true;
181
185
  }
186
+ // 用户 2026-11-04「日志少打印点吧」⇒ 这一颗的取回入口⛔ 不出声:整层重建一次几十颗,逐颗一行会刷屏。
187
+ // 文件名改由两个批次入口各打一条 —— 单颗动作(上传/复制/换地址)在下面的 `setModelUrl`,
188
+ // 整层重建在 `MktMesh.setModels`(那边一条列全)⇒ 一颗不落,但一拍一条。
182
189
  // 取回失败(404 / 解析报错都从这一档走):`threeLoader.loadModel` 是会 reject 的,⚠️ 必须当「落定在占位方块」处理
183
190
  // —— 整层重建时这一颗是压着不亮的,不落定就永远隐形(旧写法里它至少还露得出占位方块)。
184
191
  let obj;
@@ -195,6 +202,9 @@ export class ModelSlot extends Group {
195
202
  return false;
196
203
  }
197
204
  if (!obj) {
205
+ // 画面停在占位方块之外,还要让页面知道「这一颗没出来」:报进加载状态那唯一出口,带上槽名。
206
+ // 同一地址重复失败由 `threeLoader.reportLoadError` 去重(下载本身失败已由 manager 计过一次 ⇒ 这里只补「是哪颗槽」)。
207
+ threeLoader.reportLoadError(url, "model", this.name);
198
208
  this._loaded = true;
199
209
  this._layout();
200
210
  return false;
@@ -221,6 +231,11 @@ export class ModelSlot extends Group {
221
231
  * @returns 是否真的挂上了(地址无效/载入失败/这次结果已作废 ⇒ false;空地址 = 退回占位,返回 true)
222
232
  */
223
233
  async setModelUrl(url) {
234
+ // 用户 2026-11-04「日志少打印点吧」⇒ 逐颗一行从 `loadModel` 挪到这里:这一路一次动作只动一/几颗
235
+ // (上传 .glb、复制模型、换同名 glb),一条就是他能对上号的那一颗;整层重建那几十颗由
236
+ // `MktMesh.setModels` 攒成一条 ⇒ 文件名仍「一颗不落」,行数从 N 降到 1。空地址 = 退回占位,不打。
237
+ if (url)
238
+ console.log(`[MktMesh] 加载 glb「${this.name}」:${glbFileName(url)}`);
224
239
  if (!(await this.loadModel(url)))
225
240
  return false;
226
241
  if (this._staged)
@@ -42,6 +42,11 @@ import threeLoader from "../threeLoader";
42
42
  import threeUtils from "../threeUtils";
43
43
  import BoxLine2 from "./boxLine2";
44
44
 
45
+ /** glb 地址 → 文件名(日志唯一口径:去掉查询串取最后一段;⛔ 别在打印点各写一份 split) */
46
+ export function glbFileName(url: string): string {
47
+ return url.split("?")[0].split("/").pop() || url;
48
+ }
49
+
45
50
  /** 模型槽的持久化数据(进 exportData / importData / 预览 loadData) */
46
51
  export interface ModelSlotData {
47
52
  name?: string;
@@ -219,6 +224,9 @@ export class ModelSlot extends Group {
219
224
  this._loaded = true;
220
225
  return true;
221
226
  }
227
+ // 用户 2026-11-04「日志少打印点吧」⇒ 这一颗的取回入口⛔ 不出声:整层重建一次几十颗,逐颗一行会刷屏。
228
+ // 文件名改由两个批次入口各打一条 —— 单颗动作(上传/复制/换地址)在下面的 `setModelUrl`,
229
+ // 整层重建在 `MktMesh.setModels`(那边一条列全)⇒ 一颗不落,但一拍一条。
222
230
  // 取回失败(404 / 解析报错都从这一档走):`threeLoader.loadModel` 是会 reject 的,⚠️ 必须当「落定在占位方块」处理
223
231
  // —— 整层重建时这一颗是压着不亮的,不落定就永远隐形(旧写法里它至少还露得出占位方块)。
224
232
  let obj: Object3D | undefined;
@@ -233,6 +241,9 @@ export class ModelSlot extends Group {
233
241
  return false;
234
242
  }
235
243
  if (!obj) {
244
+ // 画面停在占位方块之外,还要让页面知道「这一颗没出来」:报进加载状态那唯一出口,带上槽名。
245
+ // 同一地址重复失败由 `threeLoader.reportLoadError` 去重(下载本身失败已由 manager 计过一次 ⇒ 这里只补「是哪颗槽」)。
246
+ threeLoader.reportLoadError(url, "model", this.name);
236
247
  this._loaded = true;
237
248
  this._layout();
238
249
  return false;
@@ -260,6 +271,10 @@ export class ModelSlot extends Group {
260
271
  * @returns 是否真的挂上了(地址无效/载入失败/这次结果已作废 ⇒ false;空地址 = 退回占位,返回 true)
261
272
  */
262
273
  async setModelUrl(url: string): Promise<boolean> {
274
+ // 用户 2026-11-04「日志少打印点吧」⇒ 逐颗一行从 `loadModel` 挪到这里:这一路一次动作只动一/几颗
275
+ // (上传 .glb、复制模型、换同名 glb),一条就是他能对上号的那一颗;整层重建那几十颗由
276
+ // `MktMesh.setModels` 攒成一条 ⇒ 文件名仍「一颗不落」,行数从 N 降到 1。空地址 = 退回占位,不打。
277
+ if (url) console.log(`[MktMesh] 加载 glb「${this.name}」:${glbFileName(url)}`);
263
278
  if (!(await this.loadModel(url))) return false;
264
279
  if (this._staged) this.mountStaged();
265
280
  return true;
@@ -1,4 +1,5 @@
1
1
  import { DefaultLoadingManager, Texture } from "three";
2
+ import threeLoader from "../threeLoader";
2
3
  /**
3
4
  * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
4
5
  *
@@ -25,8 +26,11 @@ export function loadPanoTexture(url) {
25
26
  DefaultLoadingManager.itemStart(url);
26
27
  const task = fetch(url)
27
28
  .then((res) => {
28
- if (!res.ok)
29
+ // 状态码先记进 `threeLoader`(页面据此分「服务器明说没有」与「断网」:前者才敢删那条数据)
30
+ if (!res.ok) {
31
+ threeLoader.markUrlStatus(url, res.status);
29
32
  throw new Error(`HTTP ${res.status}`);
33
+ }
30
34
  return res.blob();
31
35
  })
32
36
  .then((blob) => createImageBitmap(blob, {
@@ -1,4 +1,5 @@
1
1
  import { DefaultLoadingManager, Texture } from "three";
2
+ import threeLoader from "../threeLoader";
2
3
 
3
4
  /**
4
5
  * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
@@ -27,7 +28,11 @@ export function loadPanoTexture(url: string): Promise<Texture> {
27
28
  DefaultLoadingManager.itemStart(url);
28
29
  const task = fetch(url)
29
30
  .then((res) => {
30
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
31
+ // 状态码先记进 `threeLoader`(页面据此分「服务器明说没有」与「断网」:前者才敢删那条数据)
32
+ if (!res.ok) {
33
+ threeLoader.markUrlStatus(url, res.status);
34
+ throw new Error(`HTTP ${res.status}`);
35
+ }
31
36
  return res.blob();
32
37
  })
33
38
  .then((blob) =>
package/ThreeBase.js CHANGED
@@ -971,10 +971,8 @@ export default class ThreeBase extends EventDispatcher {
971
971
  // compare → R=全局 base 深度,G=最近一次 glow/stroke 选区的 select 深度。两路任一全白即该侧写入失败。
972
972
  // judge → R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)。
973
973
  // strokeSelect → 【描边步骤 1】选区本体深度(selectDepthTexture),白色=空背景、灰=物体本体。
974
- // strokeSelectBlur → 【描边步骤 5.5】描边外扩后的选区深度(selectDepthBlurTextureTarget),
975
- // 物体轮廓比本体档更胖一圈即外扩生效。
976
974
  // 通用控制台入口(任意页面可用,优先级高于面板属性):
977
- // `window.__TB_DEBUG_DEPTH = "global" | "ao" | "compare" | "judge" | "strokeSelect" | "strokeSelectBlur"`,
975
+ // `window.__TB_DEBUG_DEPTH = "global" | "ao" | "compare" | "judge" | "strokeSelect"`,
978
976
  // 设回 "none" 恢复。
979
977
  const dbgWin = window.__TB_DEBUG_DEPTH;
980
978
  const dbgMode = window.__BASE_DEPTH_VIS
@@ -1011,13 +1009,6 @@ export default class ThreeBase extends EventDispatcher {
1011
1009
  debugDepthState.lastSelectDepth ?? this.baseDepthTexture;
1012
1010
  fsQuad.render(this.renderer, baseDepthVisShader);
1013
1011
  }
1014
- else if (dbgMode === "strokeSelectBlur") {
1015
- // 描边步骤 5.5:外扩后的选区深度(RGBA8 目标,对数深度存在 .r 通道)。
1016
- // 与本体档对比看:外扩生效时物体轮廓会「胖一圈」,被遮挡的外扩环则被判 discard 后不参与。
1017
- baseDepthVisShader.uniforms.tDepth.value =
1018
- debugDepthState.lastSelectBlurDepth ?? this.baseDepthTexture;
1019
- fsQuad.render(this.renderer, baseDepthVisShader);
1020
- }
1021
1012
  flushLensflareOverlays(this.renderer, camera);
1022
1013
  return;
1023
1014
  }
package/ThreeBase.ts CHANGED
@@ -1071,10 +1071,8 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
1071
1071
  // compare → R=全局 base 深度,G=最近一次 glow/stroke 选区的 select 深度。两路任一全白即该侧写入失败。
1072
1072
  // judge → R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)。
1073
1073
  // strokeSelect → 【描边步骤 1】选区本体深度(selectDepthTexture),白色=空背景、灰=物体本体。
1074
- // strokeSelectBlur → 【描边步骤 5.5】描边外扩后的选区深度(selectDepthBlurTextureTarget),
1075
- // 物体轮廓比本体档更胖一圈即外扩生效。
1076
1074
  // 通用控制台入口(任意页面可用,优先级高于面板属性):
1077
- // `window.__TB_DEBUG_DEPTH = "global" | "ao" | "compare" | "judge" | "strokeSelect" | "strokeSelectBlur"`,
1075
+ // `window.__TB_DEBUG_DEPTH = "global" | "ao" | "compare" | "judge" | "strokeSelect"`,
1078
1076
  // 设回 "none" 恢复。
1079
1077
  const dbgWin = (window as unknown as { __TB_DEBUG_DEPTH?: DebugDepthMode }).__TB_DEBUG_DEPTH;
1080
1078
  const dbgMode = (window as unknown as { __BASE_DEPTH_VIS?: boolean }).__BASE_DEPTH_VIS
@@ -1106,12 +1104,6 @@ export default class ThreeBase extends EventDispatcher<ThreeBaseEventTypeMap> {
1106
1104
  baseDepthVisShader.uniforms.tDepth.value =
1107
1105
  debugDepthState.lastSelectDepth ?? this.baseDepthTexture;
1108
1106
  fsQuad.render(this.renderer, baseDepthVisShader);
1109
- } else if (dbgMode === "strokeSelectBlur") {
1110
- // 描边步骤 5.5:外扩后的选区深度(RGBA8 目标,对数深度存在 .r 通道)。
1111
- // 与本体档对比看:外扩生效时物体轮廓会「胖一圈」,被遮挡的外扩环则被判 discard 后不参与。
1112
- baseDepthVisShader.uniforms.tDepth.value =
1113
- debugDepthState.lastSelectBlurDepth ?? this.baseDepthTexture;
1114
- fsQuad.render(this.renderer, baseDepthVisShader);
1115
1107
  }
1116
1108
  flushLensflareOverlays(this.renderer, camera);
1117
1109
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-base",
3
- "version": "2.0.196",
3
+ "version": "2.0.198",
4
4
  "description": "A Three.js extension library providing common 3D components, animations, materials, and utilities",
5
5
  "author": "xiaosihan",
6
6
  "publishConfig": {
@@ -1,4 +1,4 @@
1
- import type { DepthTexture, Texture } from "three";
1
+ import type { DepthTexture } from "three";
2
2
  /**
3
3
  * 深度调试预览模式(面板按钮与控制台 `window.__TB_DEBUG_DEPTH` 共用同一枚举):
4
4
  * - "none" :正常渲染(默认)
@@ -7,22 +7,18 @@ import type { DepthTexture, Texture } from "three";
7
7
  * - "compare" :R=base 深度、G=选区 select 深度(对数反解后归一化)
8
8
  * - "judge" :R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)
9
9
  * - "strokeSelect" :【描边步骤 1】选区本体深度(selectDepthTexture,mask 那趟光栅化顺带写入)
10
- * - "strokeSelectBlur":【描边步骤 5.5】描边外扩后的选区深度(selectDepthBlurTextureTarget,
11
- * RGBA8 的 R 通道存对数深度,外扩环取邻域最近深度)
12
10
  * ⚠️ 2026-10-18 皇上旨「所有的对象都要进入到深度图里面了,不需要用参数过滤掉了」⇒ 原 "excluded"
13
11
  * 档(把被排除的面叠红)连同 ssao_pass 的排除判据整条删除,已无「写不进深度图的面」可标。
12
+ * ⚠️ 2026-11-04 描边回到年初口径 ⇒ 原 "strokeSelectBlur" 档(步骤 5.5 外扩后的选区深度)连同
13
+ * 那条 min-blur 整条删除,管线里已无「外扩深度」可供显示。
14
14
  */
15
- export type DebugDepthMode = "none" | "global" | "ao" | "compare" | "judge" | "strokeSelect" | "strokeSelectBlur";
15
+ export type DebugDepthMode = "none" | "global" | "ao" | "compare" | "judge" | "strokeSelect";
16
16
  /**
17
17
  * 调试用全局状态:记录最近一次 glow_pass / stroke_pass 渲染的「选区自身深度图」(selectDepth)。
18
18
  * 供 ThreeBase 渲染末尾的「辉光深度比对」调试视图读取(R=全局 base 深度,G=选区 select 深度),
19
19
  * 让用户一眼看出到底是「全局 base 深度没写进」还是「选区自身深度没写进」。
20
20
  * 由各 pass 在渲染完 selectDepth 后写入;每帧会被最后一个被处理的可见对象覆盖(调试够用)。
21
- *
22
- * lastSelectBlurDepth:描边专属(步骤 5.5 外扩后的选区深度)。它是 RGBA8 目标(不是 DepthTexture),
23
- * 深度值存在 .r 通道里 —— 可视化时同样取 .r 即可。glow/cover/frosted 没有这一步,故只有 stroke_pass 写。
24
21
  */
25
22
  export declare const debugDepthState: {
26
23
  lastSelectDepth: DepthTexture | null;
27
- lastSelectBlurDepth: Texture | null;
28
24
  };
@@ -3,11 +3,7 @@
3
3
  * 供 ThreeBase 渲染末尾的「辉光深度比对」调试视图读取(R=全局 base 深度,G=选区 select 深度),
4
4
  * 让用户一眼看出到底是「全局 base 深度没写进」还是「选区自身深度没写进」。
5
5
  * 由各 pass 在渲染完 selectDepth 后写入;每帧会被最后一个被处理的可见对象覆盖(调试够用)。
6
- *
7
- * lastSelectBlurDepth:描边专属(步骤 5.5 外扩后的选区深度)。它是 RGBA8 目标(不是 DepthTexture),
8
- * 深度值存在 .r 通道里 —— 可视化时同样取 .r 即可。glow/cover/frosted 没有这一步,故只有 stroke_pass 写。
9
6
  */
10
7
  export const debugDepthState = {
11
8
  lastSelectDepth: null,
12
- lastSelectBlurDepth: null,
13
9
  };
@@ -1,4 +1,4 @@
1
- import type { DepthTexture, Texture } from "three";
1
+ import type { DepthTexture } from "three";
2
2
 
3
3
  /**
4
4
  * 深度调试预览模式(面板按钮与控制台 `window.__TB_DEBUG_DEPTH` 共用同一枚举):
@@ -8,10 +8,10 @@ import type { DepthTexture, Texture } from "three";
8
8
  * - "compare" :R=base 深度、G=选区 select 深度(对数反解后归一化)
9
9
  * - "judge" :R=base 原始深度、G=select 原始深度、B=判定结果(1=判为被遮挡应 discard)
10
10
  * - "strokeSelect" :【描边步骤 1】选区本体深度(selectDepthTexture,mask 那趟光栅化顺带写入)
11
- * - "strokeSelectBlur":【描边步骤 5.5】描边外扩后的选区深度(selectDepthBlurTextureTarget,
12
- * RGBA8 的 R 通道存对数深度,外扩环取邻域最近深度)
13
11
  * ⚠️ 2026-10-18 皇上旨「所有的对象都要进入到深度图里面了,不需要用参数过滤掉了」⇒ 原 "excluded"
14
12
  * 档(把被排除的面叠红)连同 ssao_pass 的排除判据整条删除,已无「写不进深度图的面」可标。
13
+ * ⚠️ 2026-11-04 描边回到年初口径 ⇒ 原 "strokeSelectBlur" 档(步骤 5.5 外扩后的选区深度)连同
14
+ * 那条 min-blur 整条删除,管线里已无「外扩深度」可供显示。
15
15
  */
16
16
  export type DebugDepthMode =
17
17
  | "none"
@@ -19,22 +19,16 @@ export type DebugDepthMode =
19
19
  | "ao"
20
20
  | "compare"
21
21
  | "judge"
22
- | "strokeSelect"
23
- | "strokeSelectBlur";
22
+ | "strokeSelect";
24
23
 
25
24
  /**
26
25
  * 调试用全局状态:记录最近一次 glow_pass / stroke_pass 渲染的「选区自身深度图」(selectDepth)。
27
26
  * 供 ThreeBase 渲染末尾的「辉光深度比对」调试视图读取(R=全局 base 深度,G=选区 select 深度),
28
27
  * 让用户一眼看出到底是「全局 base 深度没写进」还是「选区自身深度没写进」。
29
28
  * 由各 pass 在渲染完 selectDepth 后写入;每帧会被最后一个被处理的可见对象覆盖(调试够用)。
30
- *
31
- * lastSelectBlurDepth:描边专属(步骤 5.5 外扩后的选区深度)。它是 RGBA8 目标(不是 DepthTexture),
32
- * 深度值存在 .r 通道里 —— 可视化时同样取 .r 即可。glow/cover/frosted 没有这一步,故只有 stroke_pass 写。
33
29
  */
34
30
  export const debugDepthState: {
35
31
  lastSelectDepth: DepthTexture | null;
36
- lastSelectBlurDepth: Texture | null;
37
32
  } = {
38
33
  lastSelectDepth: null,
39
- lastSelectBlurDepth: null,
40
34
  };