three-base 2.0.195 → 2.0.197
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 +17 -0
- package/EventMap.ts +19 -0
- package/MktMesh/MktMesh.d.ts +93 -0
- package/MktMesh/MktMesh.js +276 -1
- package/MktMesh/MktMesh.ts +278 -0
- package/MktMesh/modelSlot.js +3 -0
- package/MktMesh/modelSlot.ts +3 -0
- package/MktMesh/panoBitmapTexture.js +5 -1
- package/MktMesh/panoBitmapTexture.ts +6 -1
- package/MktMesh/panoTileStitcher.d.ts +15 -7
- package/MktMesh/panoTileStitcher.js +39 -10
- package/MktMesh/panoTileStitcher.ts +48 -15
- package/package.json +1 -1
- package/threeLoader.d.ts +14 -0
- package/threeLoader.js +48 -3
- package/threeLoader.ts +45 -3
|
@@ -177,6 +177,29 @@ function grayToSrgbVec3(hex: string): Vector3 {
|
|
|
177
177
|
return new Vector3(byte(0), byte(2), byte(4));
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** 「这张槽还没有任何内容」时交出去的**共享**素色纹理(2026-11-01 需求「进厅不要卡一下」)。
|
|
181
|
+
*
|
|
182
|
+
* 为什么不是「先建一张全尺寸 RT、整张涂灰、再交出去」:那条路在消费方点到某个还没画过的槽时就跑一次,
|
|
183
|
+
* 而一次点屏会把名册那 8 个景点 × 每点 2 槽 = 16 个槽全点一遍 ⇒ 同一帧里建出 16 张 6144×6144 的渲染目标
|
|
184
|
+
* (单张 151MB 显存,含 mipmap ≈201MB;外加 16 次整张预涂 = 约 6 亿像素填充 + 16 条 mipmap 链)。
|
|
185
|
+
* 而这一刻屏幕上要的**只是一片素色灰**,逐像素根本看不出尺寸 ⇒ 交这张 1×1 的:
|
|
186
|
+
* 字节 = `MISSING_TILE_GRAY`(与「新建 RT 整张预涂灰」落进显存的字节逐字相同),标 `SRGBColorSpace`
|
|
187
|
+
* ⇒ 采样出来的线性值与采那张灰 RT 完全一致 ⇒ 屏幕上同一色,显存与那一帧的填充归零。
|
|
188
|
+
* 真正的 RT 退回**懒建**:`_drawPass` 画进该槽第一格瓦片时才建(建好仍立刻整张涂灰,未定义像素那条规矩一字未动)。 */
|
|
189
|
+
function createGrayStandIn(): DataTexture {
|
|
190
|
+
const s = MISSING_TILE_GRAY.replace("#", "");
|
|
191
|
+
const byte = (i: number) => parseInt(s.slice(i, i + 2), 16);
|
|
192
|
+
const tex = new DataTexture(new Uint8Array([byte(0), byte(2), byte(4), 255]), 1, 1, RGBAFormat, UnsignedByteType);
|
|
193
|
+
tex.colorSpace = SRGBColorSpace;
|
|
194
|
+
tex.wrapS = ClampToEdgeWrapping;
|
|
195
|
+
tex.wrapT = ClampToEdgeWrapping;
|
|
196
|
+
tex.minFilter = LinearFilter;
|
|
197
|
+
tex.magFilter = LinearFilter;
|
|
198
|
+
tex.generateMipmaps = false;
|
|
199
|
+
tex.needsUpdate = true;
|
|
200
|
+
return tex;
|
|
201
|
+
}
|
|
202
|
+
|
|
180
203
|
/** 全屏 quad 的片元:只干「把一片瓦片的**中间那块**原样搬进 RT 的一格」。
|
|
181
204
|
* ⛔ 不加色彩转换、不加 tone mapping(见文件头画质第 2 条)。 */
|
|
182
205
|
const STITCH_VERT = /* glsl */ `
|
|
@@ -258,17 +281,20 @@ export interface PanoStitcher {
|
|
|
258
281
|
prioritize(urls: string[], layout?: TileLayout): void;
|
|
259
282
|
/**
|
|
260
283
|
* 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
|
|
261
|
-
* ⭐ 2026-10-09
|
|
262
|
-
*
|
|
284
|
+
* ⭐ 2026-10-09 需求「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时
|
|
285
|
+
* 返回共享的 1×1 缺失灰纹理**(屏幕上与「刚整张涂过灰的那张 RT」逐字节同色)⇒ 调用方切批次的那一帧
|
|
263
286
|
* 拿到的就是**深灰底**而非 null(null 会让它沿用上一批 ⇒ 墙上闪上一个全景)。
|
|
264
|
-
*
|
|
287
|
+
* ⚠️ 2026-11-01:这一支原先是**当场建一张 6144×6144 的 RT** 再交出去 ⇒ 名册 8 点 × 2 槽 = 同一帧 16 张
|
|
288
|
+
* (≈3.2GB 显存 + 约 6 亿像素预涂),是「进厅那一下卡」的本体;现在 RT 退回 `_drawPass` 画第一格时懒建。
|
|
289
|
+
* ⛔ 没传 `renderer`(诊断/测试路径)仍旧返回 null。
|
|
265
290
|
*/
|
|
266
291
|
peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;
|
|
267
292
|
/** 每帧驱动(消费方每帧只调一次)。`renderer` = 调用方当前的 WebGLRenderer,还没捕获到就传 null
|
|
268
293
|
* ⇒ 这一帧什么都不画(⛔ 不打假读数,也⛔ 不退回任何 CPU 拼合路径)。 */
|
|
269
294
|
pump(renderer: WebGLRenderer | null): void;
|
|
270
|
-
/**
|
|
271
|
-
*
|
|
295
|
+
/** 这张槽纹理是否**不需要走「每 tick 一张上传」那道闸**(RT 附件与共享的 1×1 缺失灰都算):
|
|
296
|
+
* 消费方据此直接绑定,不再排预上传队列。
|
|
297
|
+
* ⚠️ 返回 true 的纹理绝不能喂给 `renderer.initTexture` —— 前者是 three 自己管的渲染目标附件。 */
|
|
272
298
|
resident(tex: Texture): boolean;
|
|
273
299
|
/** 该批次是否已在册(诊断/测试用) */
|
|
274
300
|
has(batch: string): boolean;
|
|
@@ -331,6 +357,8 @@ private _uploadQueued: Set<TileRec> = new Set();
|
|
|
331
357
|
private _blank: DataTexture = new DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);
|
|
332
358
|
/** 本拼合器发出去的槽纹理 = RT 附件(`resident()` 认它);⛔ 不许喂给 `renderer.initTexture` */
|
|
333
359
|
private _rtTextures: WeakSet<Texture> = new WeakSet();
|
|
360
|
+
/** 「还没画过」那一路交出去的共享 1×1 素色纹理(见 `createGrayStandIn`):一座拼合器一份,⛔ 不随组建/拆 */
|
|
361
|
+
private _grayStandIn: DataTexture = createGrayStandIn();
|
|
334
362
|
|
|
335
363
|
constructor() {
|
|
336
364
|
this._quad.frustumCulled = false; // 正交相机 + 手工摆位,⛔ 别按包围球把格子剔没了
|
|
@@ -403,30 +431,34 @@ private _uploadQueued: Set<TileRec> = new Set();
|
|
|
403
431
|
}
|
|
404
432
|
|
|
405
433
|
/** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
|
|
406
|
-
* RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。
|
|
434
|
+
* RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。
|
|
435
|
+
* 还没画过任何格时交的是**共享的 1×1 素色纹理**(屏幕上与「刚涂满缺失灰的那张 RT」同色,见 `createGrayStandIn`),
|
|
436
|
+
* ⛔ 不再为「垫一片灰」当场建一张 6144×6144 的渲染目标。 */
|
|
407
437
|
peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null {
|
|
408
438
|
const g = this._groups.get(batch);
|
|
409
439
|
if (!g) return null;
|
|
410
440
|
if (!g.drawnCount[slot]) {
|
|
411
|
-
//
|
|
441
|
+
// 需求(2026-10-09)「切换全景时上一个全景的内容要渲染成深灰」:
|
|
412
442
|
// 换批次的瞬间新全景还没画进任何格,若此处照旧返回 null,消费方 `panoSlotBind` 会
|
|
413
|
-
// **沿用上一批的 RT 纹理** ⇒
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
443
|
+
// **沿用上一批的 RT 纹理** ⇒ 墙上继续显示上一个全景(报上来的现象)。
|
|
444
|
+
// 所以这一支仍要**交出点东西**:现在交的是共享那张 1×1 素色(与缺失灰逐字节同值),
|
|
445
|
+
// 随后随 `pump` 逐格显影。观感 = 深灰 → 逐格填图,一字未变;变的是「不再为一片灰建整张 RT」。
|
|
446
|
+
// ⚠️ 2026-11-01:这一支原先是 `this._ensureRt(...)` ⇒ 一次点屏(名册 8 点 × 2 槽)就在同一帧
|
|
447
|
+
// 建出 16 张 6144² 渲染目标(≈3.2GB 显存 + 6 亿像素预涂),正是「进厅那一下卡」的本体。
|
|
448
|
+
// ⛔ 没给 renderer(诊断/测试路径)仍旧返回 null,与消费方约定的「问不到就是没就绪」一致。
|
|
418
449
|
if (!renderer) return null;
|
|
419
450
|
if (!this._renderer) return null;
|
|
420
|
-
return this.
|
|
451
|
+
return this._grayStandIn;
|
|
421
452
|
}
|
|
422
453
|
const rt = g.rts[slot];
|
|
423
454
|
return rt ? rt.texture : null;
|
|
424
455
|
}
|
|
425
456
|
|
|
426
457
|
/** ⭐ 这条路交出去的槽纹理本来就是 GL 渲染目标的附件 —— 像素已经在显存里,没有「上传」这一步。
|
|
427
|
-
* 消费方(`MktMesh._peekTileAt` / 编辑器 `_peekTileAt`)据此**跳过**「每 tick 一张 `initTexture`」那道闸。
|
|
458
|
+
* 消费方(`MktMesh._peekTileAt` / 编辑器 `_peekTileAt`)据此**跳过**「每 tick 一张 `initTexture`」那道闸。
|
|
459
|
+
* 「还没画过」那一路交的共享 1×1 素色同样认(它本来就只是给 sampler 垫一个合法纹理,⛔ 别把它排进上传队列)。 */
|
|
428
460
|
resident(tex: Texture): boolean {
|
|
429
|
-
return this._rtTextures.has(tex);
|
|
461
|
+
return this._rtTextures.has(tex) || tex === this._grayStandIn;
|
|
430
462
|
}
|
|
431
463
|
|
|
432
464
|
has(batch: string): boolean {
|
|
@@ -476,6 +508,7 @@ private _uploadQueued: Set<TileRec> = new Set();
|
|
|
476
508
|
this._quad.geometry.dispose();
|
|
477
509
|
this._mat.dispose();
|
|
478
510
|
this._blank.dispose();
|
|
511
|
+
this._grayStandIn.dispose();
|
|
479
512
|
this._scene.remove(this._quad);
|
|
480
513
|
}
|
|
481
514
|
|
package/package.json
CHANGED
package/threeLoader.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ import { PLYLoader } from "three/examples/jsm/Addons";
|
|
|
9
9
|
import type { ThreeLoaderEventMap } from "./EventMap";
|
|
10
10
|
declare class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
11
11
|
constructor();
|
|
12
|
+
/** 已经报过的失败地址(去重只服务「计数」,不影响事件本身 —— 事件仍每次发,页面可按 `isNew` 决定要不要提示) */
|
|
13
|
+
private _failedUrls;
|
|
14
|
+
/** 全站累计**失败地址数**(与 manager 的 itemsTotal/itemsLoaded 同口径:累加、永不清零 ⇒ 页面自己算「本次」) */
|
|
15
|
+
loadErrorCount: number;
|
|
16
|
+
/** 地址 → 服务端回的状态码。只有**拿到过回复**的那两路写得进来:`loadPanoTexture` 的非 2xx(全景整图/32 片切片、平面 4 片)
|
|
17
|
+
* 与 `loadModel` 的下载失败(GLB 等)。用来把「服务器明说没有」和「断网 / 解码失败」分开 —— 前者页面才敢删数据。 */
|
|
18
|
+
private _urlStatus;
|
|
19
|
+
/** 记下一个地址的服务端状态码(键必须与失败上报用的那一个地址串一致) */
|
|
20
|
+
markUrlStatus(url: string, status: number): void;
|
|
21
|
+
/** 失败的唯一上报口:`DefaultLoadingManager.onError`(素材那一路)与 `ModelSlot` 那颗 GLB 没出来(模型那一路)都走这里。 */
|
|
22
|
+
reportLoadError(url: string, kind: "asset" | "model", label?: string): void;
|
|
23
|
+
/** 从加载器抛出的错误里取 HTTP 状态码,记在**调用方那一个地址**上(`url` 原样,不是内部补全后的地址)。
|
|
24
|
+
* three 的 `FileLoader` 非 2xx 抛 `HttpError`(`err.response.status`),个别加载器只把码写进 message。 */
|
|
25
|
+
private _noteErrStatus;
|
|
12
26
|
getFilename(url: string): string;
|
|
13
27
|
uuid: string;
|
|
14
28
|
anisotropy: number;
|
package/threeLoader.js
CHANGED
|
@@ -40,6 +40,12 @@ class ThreeLoader extends EventDispatcher {
|
|
|
40
40
|
DefaultLoadingManager.onLoad = () => {
|
|
41
41
|
this.onload = true;
|
|
42
42
|
};
|
|
43
|
+
// 资源加载失败:走 manager 计数的每一条(全景整图/32 片切片、图片平面 4 片、GLB 下载与内嵌贴图)
|
|
44
|
+
// 都只从这里出,页面侧仍只订本类这一个出口(与 onProgress 同规矩,⛔ 页面不许自己占 manager 的槽位)。
|
|
45
|
+
// 同一地址去重:全景/平面那两路失败会每帧重发请求 ⇒ 不去重「失败数」会被刷成无穷大。
|
|
46
|
+
DefaultLoadingManager.onError = (url) => {
|
|
47
|
+
this.reportLoadError(url, "asset");
|
|
48
|
+
};
|
|
43
49
|
// 资源替换
|
|
44
50
|
DefaultLoadingManager.setURLModifier((url) => {
|
|
45
51
|
//全路径匹配资源
|
|
@@ -58,6 +64,40 @@ class ThreeLoader extends EventDispatcher {
|
|
|
58
64
|
return new_url;
|
|
59
65
|
});
|
|
60
66
|
}
|
|
67
|
+
/** 已经报过的失败地址(去重只服务「计数」,不影响事件本身 —— 事件仍每次发,页面可按 `isNew` 决定要不要提示) */
|
|
68
|
+
_failedUrls = new Set();
|
|
69
|
+
/** 全站累计**失败地址数**(与 manager 的 itemsTotal/itemsLoaded 同口径:累加、永不清零 ⇒ 页面自己算「本次」) */
|
|
70
|
+
loadErrorCount = 0;
|
|
71
|
+
/** 地址 → 服务端回的状态码。只有**拿到过回复**的那两路写得进来:`loadPanoTexture` 的非 2xx(全景整图/32 片切片、平面 4 片)
|
|
72
|
+
* 与 `loadModel` 的下载失败(GLB 等)。用来把「服务器明说没有」和「断网 / 解码失败」分开 —— 前者页面才敢删数据。 */
|
|
73
|
+
_urlStatus = new Map();
|
|
74
|
+
/** 记下一个地址的服务端状态码(键必须与失败上报用的那一个地址串一致) */
|
|
75
|
+
markUrlStatus(url, status) {
|
|
76
|
+
if (url)
|
|
77
|
+
this._urlStatus.set(url, status);
|
|
78
|
+
}
|
|
79
|
+
/** 失败的唯一上报口:`DefaultLoadingManager.onError`(素材那一路)与 `ModelSlot` 那颗 GLB 没出来(模型那一路)都走这里。 */
|
|
80
|
+
reportLoadError(url, kind, label) {
|
|
81
|
+
const isNew = !this._failedUrls.has(url);
|
|
82
|
+
if (isNew) {
|
|
83
|
+
this._failedUrls.add(url);
|
|
84
|
+
this.loadErrorCount++;
|
|
85
|
+
}
|
|
86
|
+
const status = this._urlStatus.get(url);
|
|
87
|
+
// 403 与 404 并一档:OSS 私有桶里「key 不存在」回的正是 403(AccessDenied 用来不泄露对象是否存在)。
|
|
88
|
+
const missing = status === 404 || status === 403;
|
|
89
|
+
this.dispatchEvent({ type: "loadError", url, kind, label, status, missing, failed: this.loadErrorCount, isNew });
|
|
90
|
+
}
|
|
91
|
+
/** 从加载器抛出的错误里取 HTTP 状态码,记在**调用方那一个地址**上(`url` 原样,不是内部补全后的地址)。
|
|
92
|
+
* three 的 `FileLoader` 非 2xx 抛 `HttpError`(`err.response.status`),个别加载器只把码写进 message。 */
|
|
93
|
+
_noteErrStatus(url, err) {
|
|
94
|
+
if (!isString(url))
|
|
95
|
+
return;
|
|
96
|
+
const e = err;
|
|
97
|
+
const raw = e?.status ?? e?.response?.status ?? Number(/(?:responded with|HTTP) (\d{3})/.exec(e?.message ?? "")?.[1]);
|
|
98
|
+
if (Number.isFinite(raw))
|
|
99
|
+
this.markUrlStatus(url, raw);
|
|
100
|
+
}
|
|
61
101
|
// 从url 中提取文件名
|
|
62
102
|
getFilename(url) {
|
|
63
103
|
// 移除URL的查询字符串和哈希部分
|
|
@@ -364,7 +404,8 @@ class ThreeLoader extends EventDispatcher {
|
|
|
364
404
|
});
|
|
365
405
|
return res;
|
|
366
406
|
})
|
|
367
|
-
.catch(() => {
|
|
407
|
+
.catch((err) => {
|
|
408
|
+
this._noteErrStatus(url, err);
|
|
368
409
|
console.error(`${url},加载失败`);
|
|
369
410
|
return undefined;
|
|
370
411
|
});
|
|
@@ -381,6 +422,7 @@ class ThreeLoader extends EventDispatcher {
|
|
|
381
422
|
return res.scene;
|
|
382
423
|
})
|
|
383
424
|
.catch((err) => {
|
|
425
|
+
this._noteErrStatus(url, err);
|
|
384
426
|
// 打印详细错误信息,方便调试
|
|
385
427
|
console.error(`${url},加载失败`, err);
|
|
386
428
|
return undefined;
|
|
@@ -398,7 +440,8 @@ class ThreeLoader extends EventDispatcher {
|
|
|
398
440
|
roughness: 0.5,
|
|
399
441
|
}));
|
|
400
442
|
})
|
|
401
|
-
.catch(() => {
|
|
443
|
+
.catch((err) => {
|
|
444
|
+
this._noteErrStatus(url, err);
|
|
402
445
|
console.error(`${url},加载失败`);
|
|
403
446
|
return undefined;
|
|
404
447
|
});
|
|
@@ -434,7 +477,8 @@ class ThreeLoader extends EventDispatcher {
|
|
|
434
477
|
}
|
|
435
478
|
return plyMesh;
|
|
436
479
|
})
|
|
437
|
-
.catch(() => {
|
|
480
|
+
.catch((err) => {
|
|
481
|
+
this._noteErrStatus(url, err);
|
|
438
482
|
console.error(`${url},加载失败`);
|
|
439
483
|
return undefined;
|
|
440
484
|
});
|
|
@@ -462,6 +506,7 @@ class ThreeLoader extends EventDispatcher {
|
|
|
462
506
|
return res;
|
|
463
507
|
})
|
|
464
508
|
.catch((err) => {
|
|
509
|
+
this._noteErrStatus(url, err);
|
|
465
510
|
console.error(`${url},加载失败`, err);
|
|
466
511
|
return undefined;
|
|
467
512
|
});
|
package/threeLoader.ts
CHANGED
|
@@ -44,6 +44,12 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
44
44
|
DefaultLoadingManager.onLoad = () => {
|
|
45
45
|
this.onload = true;
|
|
46
46
|
};
|
|
47
|
+
// 资源加载失败:走 manager 计数的每一条(全景整图/32 片切片、图片平面 4 片、GLB 下载与内嵌贴图)
|
|
48
|
+
// 都只从这里出,页面侧仍只订本类这一个出口(与 onProgress 同规矩,⛔ 页面不许自己占 manager 的槽位)。
|
|
49
|
+
// 同一地址去重:全景/平面那两路失败会每帧重发请求 ⇒ 不去重「失败数」会被刷成无穷大。
|
|
50
|
+
DefaultLoadingManager.onError = (url: string) => {
|
|
51
|
+
this.reportLoadError(url, "asset");
|
|
52
|
+
};
|
|
47
53
|
// 资源替换
|
|
48
54
|
DefaultLoadingManager.setURLModifier((url: string) => {
|
|
49
55
|
//全路径匹配资源
|
|
@@ -62,6 +68,37 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
62
68
|
return new_url;
|
|
63
69
|
});
|
|
64
70
|
}
|
|
71
|
+
/** 已经报过的失败地址(去重只服务「计数」,不影响事件本身 —— 事件仍每次发,页面可按 `isNew` 决定要不要提示) */
|
|
72
|
+
private _failedUrls: Set<string> = new Set();
|
|
73
|
+
/** 全站累计**失败地址数**(与 manager 的 itemsTotal/itemsLoaded 同口径:累加、永不清零 ⇒ 页面自己算「本次」) */
|
|
74
|
+
loadErrorCount: number = 0;
|
|
75
|
+
/** 地址 → 服务端回的状态码。只有**拿到过回复**的那两路写得进来:`loadPanoTexture` 的非 2xx(全景整图/32 片切片、平面 4 片)
|
|
76
|
+
* 与 `loadModel` 的下载失败(GLB 等)。用来把「服务器明说没有」和「断网 / 解码失败」分开 —— 前者页面才敢删数据。 */
|
|
77
|
+
private _urlStatus: Map<string, number> = new Map();
|
|
78
|
+
/** 记下一个地址的服务端状态码(键必须与失败上报用的那一个地址串一致) */
|
|
79
|
+
markUrlStatus(url: string, status: number): void {
|
|
80
|
+
if (url) this._urlStatus.set(url, status);
|
|
81
|
+
}
|
|
82
|
+
/** 失败的唯一上报口:`DefaultLoadingManager.onError`(素材那一路)与 `ModelSlot` 那颗 GLB 没出来(模型那一路)都走这里。 */
|
|
83
|
+
reportLoadError(url: string, kind: "asset" | "model", label?: string): void {
|
|
84
|
+
const isNew = !this._failedUrls.has(url);
|
|
85
|
+
if (isNew) {
|
|
86
|
+
this._failedUrls.add(url);
|
|
87
|
+
this.loadErrorCount++;
|
|
88
|
+
}
|
|
89
|
+
const status = this._urlStatus.get(url);
|
|
90
|
+
// 403 与 404 并一档:OSS 私有桶里「key 不存在」回的正是 403(AccessDenied 用来不泄露对象是否存在)。
|
|
91
|
+
const missing = status === 404 || status === 403;
|
|
92
|
+
this.dispatchEvent({ type: "loadError", url, kind, label, status, missing, failed: this.loadErrorCount, isNew });
|
|
93
|
+
}
|
|
94
|
+
/** 从加载器抛出的错误里取 HTTP 状态码,记在**调用方那一个地址**上(`url` 原样,不是内部补全后的地址)。
|
|
95
|
+
* three 的 `FileLoader` 非 2xx 抛 `HttpError`(`err.response.status`),个别加载器只把码写进 message。 */
|
|
96
|
+
private _noteErrStatus(url: string | File | Array<File>, err: unknown): void {
|
|
97
|
+
if (!isString(url)) return;
|
|
98
|
+
const e = err as { status?: number; response?: { status?: number }; message?: string };
|
|
99
|
+
const raw = e?.status ?? e?.response?.status ?? Number(/(?:responded with|HTTP) (\d{3})/.exec(e?.message ?? "")?.[1]);
|
|
100
|
+
if (Number.isFinite(raw)) this.markUrlStatus(url, raw);
|
|
101
|
+
}
|
|
65
102
|
// 从url 中提取文件名
|
|
66
103
|
getFilename(url: string): string {
|
|
67
104
|
// 移除URL的查询字符串和哈希部分
|
|
@@ -370,7 +407,8 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
370
407
|
});
|
|
371
408
|
return res;
|
|
372
409
|
})
|
|
373
|
-
.catch(() => {
|
|
410
|
+
.catch((err) => {
|
|
411
|
+
this._noteErrStatus(url, err);
|
|
374
412
|
console.error(`${url},加载失败`);
|
|
375
413
|
return undefined;
|
|
376
414
|
});
|
|
@@ -387,6 +425,7 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
387
425
|
return res.scene;
|
|
388
426
|
})
|
|
389
427
|
.catch((err) => {
|
|
428
|
+
this._noteErrStatus(url, err);
|
|
390
429
|
// 打印详细错误信息,方便调试
|
|
391
430
|
console.error(`${url},加载失败`, err);
|
|
392
431
|
return undefined;
|
|
@@ -404,7 +443,8 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
404
443
|
roughness: 0.5,
|
|
405
444
|
}));
|
|
406
445
|
})
|
|
407
|
-
.catch(() => {
|
|
446
|
+
.catch((err) => {
|
|
447
|
+
this._noteErrStatus(url, err);
|
|
408
448
|
console.error(`${url},加载失败`);
|
|
409
449
|
return undefined;
|
|
410
450
|
});
|
|
@@ -440,7 +480,8 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
440
480
|
}
|
|
441
481
|
return plyMesh;
|
|
442
482
|
})
|
|
443
|
-
.catch(() => {
|
|
483
|
+
.catch((err) => {
|
|
484
|
+
this._noteErrStatus(url, err);
|
|
444
485
|
console.error(`${url},加载失败`);
|
|
445
486
|
return undefined;
|
|
446
487
|
});
|
|
@@ -467,6 +508,7 @@ class ThreeLoader extends EventDispatcher<ThreeLoaderEventMap> {
|
|
|
467
508
|
return res;
|
|
468
509
|
})
|
|
469
510
|
.catch((err) => {
|
|
511
|
+
this._noteErrStatus(url, err);
|
|
470
512
|
console.error(`${url},加载失败`, err);
|
|
471
513
|
return undefined;
|
|
472
514
|
});
|