three-base 2.0.190 → 2.0.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import { Mesh, MeshPhysicalMaterial, PlaneGeometry, Texture } from "three";
2
+ /** 玻璃面的持久化数据(进 `MktMesh.exportData` 的 `glass` 键 / `importData` 还原) */
3
+ export interface GlassPaneData {
4
+ name?: string;
5
+ position: {
6
+ x: number;
7
+ y: number;
8
+ z: number;
9
+ };
10
+ rotation: {
11
+ x: number;
12
+ y: number;
13
+ z: number;
14
+ };
15
+ /** 正方形边长(米):宽 = 高 = 本值,⛔ 不拆成两档 */
16
+ size: number;
17
+ }
18
+ export declare class GlassPane extends Mesh<PlaneGeometry, MeshPhysicalMaterial> {
19
+ private _originalColor;
20
+ /** 反射环境纹理的身份(由 `MktMesh` 注入,本类只持有引用、⛔ 从不 dispose 它 —— 那张按地址缓存在库里) */
21
+ private _envTex;
22
+ constructor(name?: string);
23
+ get paneSize(): number;
24
+ /** 换正方形边长(宽高等比跟着走:几何只有这一把尺) */
25
+ setPaneSize(size: number): void;
26
+ /**
27
+ * 绑/摘反射环境纹理(`null` = 还没有可用的环境源)。
28
+ * ⚠️ 判等在前:`material.needsUpdate = true` 会涨材质版本 ⇒ 程序要重编译。库里给同一批玻璃面注入的是
29
+ * **同一个 Texture 对象**(反射源只下载一次),逐颗重复赋同一张时必须早退,不然就是 N 次无谓重编译。
30
+ * ⚠️ 传进来的纹理必须已标 `EquirectangularReflectionMapping`:three 的 `WebGLCubeUVMaps` 只认那个标记才会
31
+ * 自动把它卷积成立方 UV(PMREM);直接给一张普通 UV 纹理(例如全景拼合槽的那两张 RT)采出来是花屏。
32
+ */
33
+ setEnvironmentTexture(tex: Texture | null): void;
34
+ highlight(): void;
35
+ restoreColor(): void;
36
+ toData(): GlassPaneData;
37
+ applyData(d: GlassPaneData): void;
38
+ /** 只释放自己的几何与材质;反射纹理归 `MktMesh` 管(多颗共用一张)。 */
39
+ dispose(): void;
40
+ }
41
+ export declare const GLASS_PANE_DEFAULTS: {
42
+ size: number;
43
+ };
@@ -0,0 +1,97 @@
1
+ /*
2
+ * @Description: 正方形玻璃面 —— 2026-10-28 需求「mkteditor 还需要增加一个 正方的面的 功能, 要双面显示,
3
+ * 环境贴图就用第一个全景, 效果用 three.js 的 webgl_materials_physical_transmission 案例, 并且要写入 json 数据和回显」。
4
+ *
5
+ * 定位:与「图片平面」(`./imagePlane.ts`)同为挂在 `MktMesh` 子树下的独立 Mesh,但材质完全不同:
6
+ * - 图片平面 = MeshBasicMaterial + FrontSide + 自带内容瓦片(单面可见是 2026-10-22 单独点名的,⛔ 不适用于本类);
7
+ * - 玻璃面 = MeshPhysicalMaterial + DoubleSide + transmission(本次点名「要双面显示」⇒ 正反两面都画、都折射)。
8
+ * 它自己没有内容纹理:画面 = 背后的东西被折射 + 一层反射,反射来自 `MktMesh` 给的**第一个全景**(`setEnvironmentTexture`)。
9
+ *
10
+ * 材质参数逐字对齐官方示例 `webgl_materials_physical_transmission` 的默认档
11
+ * (transmission 1 / ior 1.5 / thickness 0.01 / roughness 0 / metalness 0 / specularIntensity 1 /
12
+ * specularColor 白 / envMapIntensity 1 / color 白 / opacity 1 / transparent true / side DoubleSide)。
13
+ * ⛔ 示例 GUI 里那颗 `transmissionResolutionScale` 在 three 0.167 的构建里不存在(属主渲染器且该版本无此属性),
14
+ * 写了会直接把类型检查打红 ⇒ 本文件与宿主都不设它,折射全屏缓冲用引擎默认分辨率。
15
+ * ⛔ 不设 `alphaMap`:示例把它挂在那颗圆环上(切面形状用),玻璃面就是一块方板,没有alpha 轮廓要描。
16
+ *
17
+ * 形状:正方形 ⇒ 存档只有**一个**边长 `size`(米),宽恒等于高。几何仍是 PlaneGeometry(size, size),
18
+ * 原点在板中心,落地/贴墙那两种落点由调用者摆(与图片平面同一条口径,见编辑器 `addGlassPaneAt`)。
19
+ */
20
+ import { Color, DoubleSide, Mesh, MeshPhysicalMaterial, PlaneGeometry } from "three";
21
+ const DEFAULT_SIZE = 1.5;
22
+ /** 选中高亮色:与图片平面/模型槽同一档(同族观感一致,只换颜色不换形态) */
23
+ const HIGHLIGHT_COLOR = "#ffd24a";
24
+ export class GlassPane extends Mesh {
25
+ _originalColor = "#ffffff";
26
+ /** 反射环境纹理的身份(由 `MktMesh` 注入,本类只持有引用、⛔ 从不 dispose 它 —— 那张按地址缓存在库里) */
27
+ _envTex = null;
28
+ constructor(name) {
29
+ const geo = new PlaneGeometry(DEFAULT_SIZE, DEFAULT_SIZE);
30
+ const mat = new MeshPhysicalMaterial({
31
+ color: new Color("#ffffff"),
32
+ // 双面(本次点名「要双面显示」):正面反面都画,也都能被射线打中
33
+ //(three 的射线拾取按材质 side 剔背面,见 `node_modules/three/src/objects/Mesh.js`)。
34
+ side: DoubleSide,
35
+ transparent: true,
36
+ opacity: 1,
37
+ // ↓ 以下六档 + 上面两档 = 官方示例的默认 GUI 值,一字未改
38
+ transmission: 1,
39
+ ior: 1.5,
40
+ thickness: 0.01,
41
+ roughness: 0,
42
+ metalness: 0,
43
+ specularIntensity: 1,
44
+ specularColor: new Color("#ffffff"),
45
+ envMapIntensity: 1,
46
+ });
47
+ super(geo, mat);
48
+ this.name = name || "玻璃面";
49
+ }
50
+ get paneSize() { return this.geometry.parameters.width; }
51
+ /** 换正方形边长(宽高等比跟着走:几何只有这一把尺) */
52
+ setPaneSize(size) {
53
+ this.geometry.dispose();
54
+ this.geometry = new PlaneGeometry(size, size);
55
+ }
56
+ /**
57
+ * 绑/摘反射环境纹理(`null` = 还没有可用的环境源)。
58
+ * ⚠️ 判等在前:`material.needsUpdate = true` 会涨材质版本 ⇒ 程序要重编译。库里给同一批玻璃面注入的是
59
+ * **同一个 Texture 对象**(反射源只下载一次),逐颗重复赋同一张时必须早退,不然就是 N 次无谓重编译。
60
+ * ⚠️ 传进来的纹理必须已标 `EquirectangularReflectionMapping`:three 的 `WebGLCubeUVMaps` 只认那个标记才会
61
+ * 自动把它卷积成立方 UV(PMREM);直接给一张普通 UV 纹理(例如全景拼合槽的那两张 RT)采出来是花屏。
62
+ */
63
+ setEnvironmentTexture(tex) {
64
+ if (this._envTex === tex)
65
+ return;
66
+ this._envTex = tex;
67
+ this.material.envMap = tex;
68
+ this.material.needsUpdate = true;
69
+ }
70
+ highlight() {
71
+ this.material.color.set(HIGHLIGHT_COLOR);
72
+ }
73
+ restoreColor() {
74
+ this.material.color.set(this._originalColor);
75
+ }
76
+ toData() {
77
+ return {
78
+ name: this.name || undefined,
79
+ position: { x: this.position.x, y: this.position.y, z: this.position.z },
80
+ rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
81
+ size: this.paneSize,
82
+ };
83
+ }
84
+ applyData(d) {
85
+ if (d.name)
86
+ this.name = d.name;
87
+ this.position.set(d.position.x, d.position.y, d.position.z);
88
+ this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
89
+ this.setPaneSize(d.size);
90
+ }
91
+ /** 只释放自己的几何与材质;反射纹理归 `MktMesh` 管(多颗共用一张)。 */
92
+ dispose() {
93
+ this.geometry.dispose();
94
+ this.material.dispose();
95
+ }
96
+ }
97
+ export const GLASS_PANE_DEFAULTS = { size: DEFAULT_SIZE };
@@ -0,0 +1,116 @@
1
+ /*
2
+ * @Description: 正方形玻璃面 —— 2026-10-28 需求「mkteditor 还需要增加一个 正方的面的 功能, 要双面显示,
3
+ * 环境贴图就用第一个全景, 效果用 three.js 的 webgl_materials_physical_transmission 案例, 并且要写入 json 数据和回显」。
4
+ *
5
+ * 定位:与「图片平面」(`./imagePlane.ts`)同为挂在 `MktMesh` 子树下的独立 Mesh,但材质完全不同:
6
+ * - 图片平面 = MeshBasicMaterial + FrontSide + 自带内容瓦片(单面可见是 2026-10-22 单独点名的,⛔ 不适用于本类);
7
+ * - 玻璃面 = MeshPhysicalMaterial + DoubleSide + transmission(本次点名「要双面显示」⇒ 正反两面都画、都折射)。
8
+ * 它自己没有内容纹理:画面 = 背后的东西被折射 + 一层反射,反射来自 `MktMesh` 给的**第一个全景**(`setEnvironmentTexture`)。
9
+ *
10
+ * 材质参数逐字对齐官方示例 `webgl_materials_physical_transmission` 的默认档
11
+ * (transmission 1 / ior 1.5 / thickness 0.01 / roughness 0 / metalness 0 / specularIntensity 1 /
12
+ * specularColor 白 / envMapIntensity 1 / color 白 / opacity 1 / transparent true / side DoubleSide)。
13
+ * ⛔ 示例 GUI 里那颗 `transmissionResolutionScale` 在 three 0.167 的构建里不存在(属主渲染器且该版本无此属性),
14
+ * 写了会直接把类型检查打红 ⇒ 本文件与宿主都不设它,折射全屏缓冲用引擎默认分辨率。
15
+ * ⛔ 不设 `alphaMap`:示例把它挂在那颗圆环上(切面形状用),玻璃面就是一块方板,没有alpha 轮廓要描。
16
+ *
17
+ * 形状:正方形 ⇒ 存档只有**一个**边长 `size`(米),宽恒等于高。几何仍是 PlaneGeometry(size, size),
18
+ * 原点在板中心,落地/贴墙那两种落点由调用者摆(与图片平面同一条口径,见编辑器 `addGlassPaneAt`)。
19
+ */
20
+ import { Color, DoubleSide, Mesh, MeshPhysicalMaterial, PlaneGeometry, Texture } from "three";
21
+
22
+ /** 玻璃面的持久化数据(进 `MktMesh.exportData` 的 `glass` 键 / `importData` 还原) */
23
+ export interface GlassPaneData {
24
+ name?: string;
25
+ position: { x: number; y: number; z: number };
26
+ rotation: { x: number; y: number; z: number };
27
+ /** 正方形边长(米):宽 = 高 = 本值,⛔ 不拆成两档 */
28
+ size: number;
29
+ }
30
+
31
+ const DEFAULT_SIZE = 1.5;
32
+ /** 选中高亮色:与图片平面/模型槽同一档(同族观感一致,只换颜色不换形态) */
33
+ const HIGHLIGHT_COLOR = "#ffd24a";
34
+
35
+ export class GlassPane extends Mesh<PlaneGeometry, MeshPhysicalMaterial> {
36
+ private _originalColor: string = "#ffffff";
37
+ /** 反射环境纹理的身份(由 `MktMesh` 注入,本类只持有引用、⛔ 从不 dispose 它 —— 那张按地址缓存在库里) */
38
+ private _envTex: Texture | null = null;
39
+
40
+ constructor(name?: string) {
41
+ const geo = new PlaneGeometry(DEFAULT_SIZE, DEFAULT_SIZE);
42
+ const mat = new MeshPhysicalMaterial({
43
+ color: new Color("#ffffff"),
44
+ // 双面(本次点名「要双面显示」):正面反面都画,也都能被射线打中
45
+ //(three 的射线拾取按材质 side 剔背面,见 `node_modules/three/src/objects/Mesh.js`)。
46
+ side: DoubleSide,
47
+ transparent: true,
48
+ opacity: 1,
49
+ // ↓ 以下六档 + 上面两档 = 官方示例的默认 GUI 值,一字未改
50
+ transmission: 1,
51
+ ior: 1.5,
52
+ thickness: 0.01,
53
+ roughness: 0,
54
+ metalness: 0,
55
+ specularIntensity: 1,
56
+ specularColor: new Color("#ffffff"),
57
+ envMapIntensity: 1,
58
+ });
59
+ super(geo, mat);
60
+ this.name = name || "玻璃面";
61
+ }
62
+
63
+ get paneSize(): number { return (this.geometry as PlaneGeometry).parameters.width; }
64
+
65
+ /** 换正方形边长(宽高等比跟着走:几何只有这一把尺) */
66
+ setPaneSize(size: number): void {
67
+ this.geometry.dispose();
68
+ this.geometry = new PlaneGeometry(size, size);
69
+ }
70
+
71
+ /**
72
+ * 绑/摘反射环境纹理(`null` = 还没有可用的环境源)。
73
+ * ⚠️ 判等在前:`material.needsUpdate = true` 会涨材质版本 ⇒ 程序要重编译。库里给同一批玻璃面注入的是
74
+ * **同一个 Texture 对象**(反射源只下载一次),逐颗重复赋同一张时必须早退,不然就是 N 次无谓重编译。
75
+ * ⚠️ 传进来的纹理必须已标 `EquirectangularReflectionMapping`:three 的 `WebGLCubeUVMaps` 只认那个标记才会
76
+ * 自动把它卷积成立方 UV(PMREM);直接给一张普通 UV 纹理(例如全景拼合槽的那两张 RT)采出来是花屏。
77
+ */
78
+ setEnvironmentTexture(tex: Texture | null): void {
79
+ if (this._envTex === tex) return;
80
+ this._envTex = tex;
81
+ this.material.envMap = tex;
82
+ this.material.needsUpdate = true;
83
+ }
84
+
85
+ highlight(): void {
86
+ this.material.color.set(HIGHLIGHT_COLOR);
87
+ }
88
+
89
+ restoreColor(): void {
90
+ this.material.color.set(this._originalColor);
91
+ }
92
+
93
+ toData(): GlassPaneData {
94
+ return {
95
+ name: this.name || undefined,
96
+ position: { x: this.position.x, y: this.position.y, z: this.position.z },
97
+ rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
98
+ size: this.paneSize,
99
+ };
100
+ }
101
+
102
+ applyData(d: GlassPaneData): void {
103
+ if (d.name) this.name = d.name;
104
+ this.position.set(d.position.x, d.position.y, d.position.z);
105
+ this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
106
+ this.setPaneSize(d.size);
107
+ }
108
+
109
+ /** 只释放自己的几何与材质;反射纹理归 `MktMesh` 管(多颗共用一张)。 */
110
+ dispose(): void {
111
+ this.geometry.dispose();
112
+ this.material.dispose();
113
+ }
114
+ }
115
+
116
+ export const GLASS_PANE_DEFAULTS = { size: DEFAULT_SIZE };
@@ -37,9 +37,13 @@ export declare class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
37
37
  private _uInset;
38
38
  private _uValid;
39
39
  private _data;
40
+ /** 内容是否已落定:4 格里至少有一格真的绑上了纹理。**只朝 true 翻一次**(之后逐张显影继续,但不再回退成「没内容」)。 */
41
+ private _loaded;
40
42
  constructor(name?: string);
41
43
  get imageWidth(): number;
42
44
  get imageHeight(): number;
45
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
46
+ get loaded(): boolean;
43
47
  setPlaneSize(w: number, h: number): void;
44
48
  setPlaneData(d: Pick<ImagePlaneData, "url" | "tiles" | "naturalW" | "naturalH">): void;
45
49
  get tiles(): string[];
@@ -70,6 +70,8 @@ export class ImagePlane extends Mesh {
70
70
  url: "",
71
71
  tiles: [],
72
72
  };
73
+ /** 内容是否已落定:4 格里至少有一格真的绑上了纹理。**只朝 true 翻一次**(之后逐张显影继续,但不再回退成「没内容」)。 */
74
+ _loaded = false;
73
75
  constructor(name) {
74
76
  const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
75
77
  const mat = new MeshBasicMaterial({
@@ -108,6 +110,8 @@ export class ImagePlane extends Mesh {
108
110
  }
109
111
  get imageWidth() { return this.geometry.parameters.width; }
110
112
  get imageHeight() { return this.geometry.parameters.height; }
113
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
114
+ get loaded() { return this._loaded; }
111
115
  setPlaneSize(w, h) {
112
116
  this.geometry.dispose();
113
117
  this.geometry = new PlaneGeometry(w, h);
@@ -127,6 +131,10 @@ export class ImagePlane extends Mesh {
127
131
  if (insets[i])
128
132
  this._uInset.value[i].copy(insets[i]);
129
133
  this._uValid.value[i] = t ? 1 : 0;
134
+ // 第一格就绪 = 这一颗有东西可画了 ⇒ 落定(点亮闸认的就是这一条,⛔ 不是「4 格全到齐」:
135
+ // 「逐张就绪逐张显影」是本文件 10-22 晚定下的口径,等到全到齐会让先到的那几格白等几帧)。
136
+ if (t)
137
+ this._loaded = true;
130
138
  }
131
139
  }
132
140
  /** 只读「此刻材质里绑了什么」的戳记:4 格就绪位 + 已绑纹理的版本号。
@@ -102,6 +102,8 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
102
102
  url: "",
103
103
  tiles: [],
104
104
  };
105
+ /** 内容是否已落定:4 格里至少有一格真的绑上了纹理。**只朝 true 翻一次**(之后逐张显影继续,但不再回退成「没内容」)。 */
106
+ private _loaded = false;
105
107
 
106
108
  constructor(name?: string) {
107
109
  const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
@@ -145,6 +147,8 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
145
147
 
146
148
  get imageWidth(): number { return (this.geometry as PlaneGeometry).parameters.width; }
147
149
  get imageHeight(): number { return (this.geometry as PlaneGeometry).parameters.height; }
150
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
151
+ get loaded(): boolean { return this._loaded; }
148
152
 
149
153
  setPlaneSize(w: number, h: number): void {
150
154
  this.geometry.dispose();
@@ -167,6 +171,9 @@ export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
167
171
  this._uTiles.value[i] = t || PLACEHOLDER_TEX;
168
172
  if (insets[i]) this._uInset.value[i].copy(insets[i]);
169
173
  this._uValid.value[i] = t ? 1 : 0;
174
+ // 第一格就绪 = 这一颗有东西可画了 ⇒ 落定(点亮闸认的就是这一条,⛔ 不是「4 格全到齐」:
175
+ // 「逐张就绪逐张显影」是本文件 10-22 晚定下的口径,等到全到齐会让先到的那几格白等几帧)。
176
+ if (t) this._loaded = true;
170
177
  }
171
178
  }
172
179
 
@@ -66,6 +66,12 @@ export declare class ModelSlot extends Group {
66
66
  private _staged;
67
67
  /** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
68
68
  private _loadToken;
69
+ /**
70
+ * 内容是否已落定:真模型挂进了 `_contentRoot`,或这一颗压根没在等什么(空地址 / 取回失败 ⇒ 停在占位小立方体)。
71
+ * ⚠️ 落定**不等于**已经画进 GPU —— 顶点缓冲与首次 program 编译发生在「亮出来的那一帧」,
72
+ * 所以逐帧限流必须闸在点亮这一下(`MktMesh.tickProgressiveReveal`),闸在挂载那一下拦不住 GPU 侧的账。
73
+ */
74
+ private _loaded;
69
75
  private _disposed;
70
76
  constructor(name?: string);
71
77
  get size(): number;
@@ -78,6 +84,8 @@ export declare class ModelSlot extends Group {
78
84
  } | undefined;
79
85
  /** 是否已经挂上真模型(false = 仍是占位小立方体) */
80
86
  get hasModel(): boolean;
87
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
88
+ get loaded(): boolean;
81
89
  /** 改尺寸档(米,最长边):立刻重缩内容 */
82
90
  setSize(size: number): void;
83
91
  /**
@@ -107,6 +107,12 @@ export class ModelSlot extends Group {
107
107
  _staged = null;
108
108
  /** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
109
109
  _loadToken = 0;
110
+ /**
111
+ * 内容是否已落定:真模型挂进了 `_contentRoot`,或这一颗压根没在等什么(空地址 / 取回失败 ⇒ 停在占位小立方体)。
112
+ * ⚠️ 落定**不等于**已经画进 GPU —— 顶点缓冲与首次 program 编译发生在「亮出来的那一帧」,
113
+ * 所以逐帧限流必须闸在点亮这一下(`MktMesh.tickProgressiveReveal`),闸在挂载那一下拦不住 GPU 侧的账。
114
+ */
115
+ _loaded = false;
110
116
  _disposed = false;
111
117
  constructor(name) {
112
118
  super();
@@ -126,6 +132,8 @@ export class ModelSlot extends Group {
126
132
  get naturalSize() { return this._naturalSize; }
127
133
  /** 是否已经挂上真模型(false = 仍是占位小立方体) */
128
134
  get hasModel() { return this._model !== null; }
135
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
136
+ get loaded() { return this._loaded; }
129
137
  /** 改尺寸档(米,最长边):立刻重缩内容 */
130
138
  setSize(size) {
131
139
  this._size = size > 0 ? size : this._size;
@@ -161,13 +169,25 @@ export class ModelSlot extends Group {
161
169
  async loadModel(url) {
162
170
  this._url = url;
163
171
  const token = ++this._loadToken;
172
+ this._loaded = false;
164
173
  this._detachModel();
165
174
  // 先立刻退回占位小立方体:GLB 在路上那段窗口(慢网/大模型)⛔ 不许让这一槽凭空消失
166
175
  // (取回失败那条支路本来也是停在占位上,两段时间里画面一致)
167
176
  this._layout();
168
- if (!url)
177
+ if (!url) {
178
+ // 空地址 = 这一颗压根没在等什么 ⇒ 当场落定(占位小立方体就是它的最终内容)
179
+ this._loaded = true;
169
180
  return true;
170
- const obj = await threeLoader.loadModel(url);
181
+ }
182
+ // 取回失败(404 / 解析报错都从这一档走):`threeLoader.loadModel` 是会 reject 的,⚠️ 必须当「落定在占位方块」处理
183
+ // —— 整层重建时这一颗是压着不亮的,不落定就永远隐形(旧写法里它至少还露得出占位方块)。
184
+ let obj;
185
+ try {
186
+ obj = await threeLoader.loadModel(url);
187
+ }
188
+ catch {
189
+ obj = undefined;
190
+ }
171
191
  // 迟到回调:这一槽已经改指向别的地址、或已经删除 ⇒ 当场释放,别留一份没人认领的显存
172
192
  if (this._disposed || token !== this._loadToken) {
173
193
  if (obj)
@@ -175,6 +195,7 @@ export class ModelSlot extends Group {
175
195
  return false;
176
196
  }
177
197
  if (!obj) {
198
+ this._loaded = true;
178
199
  this._layout();
179
200
  return false;
180
201
  }
@@ -189,6 +210,7 @@ export class ModelSlot extends Group {
189
210
  this._staged = null;
190
211
  this._model = obj;
191
212
  this._contentRoot.add(obj);
213
+ this._loaded = true;
192
214
  this._layout();
193
215
  return true;
194
216
  }
@@ -220,6 +242,8 @@ export class ModelSlot extends Group {
220
242
  this._size = d.size > 0 ? d.size : MODEL_SLOT_DEFAULT_SIZE;
221
243
  this._naturalSize = d.naturalSize;
222
244
  this._url = d.url || "";
245
+ // 落定判据跟着地址走:空地址 = 没在等什么(占位方块就是最终内容);有地址 = 等 GLB 那一拍
246
+ this._loaded = !this._url;
223
247
  if (!this._url)
224
248
  this._detachModel();
225
249
  this._layout();
@@ -137,6 +137,12 @@ export class ModelSlot extends Group {
137
137
  private _staged: Object3D | null = null;
138
138
  /** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
139
139
  private _loadToken = 0;
140
+ /**
141
+ * 内容是否已落定:真模型挂进了 `_contentRoot`,或这一颗压根没在等什么(空地址 / 取回失败 ⇒ 停在占位小立方体)。
142
+ * ⚠️ 落定**不等于**已经画进 GPU —— 顶点缓冲与首次 program 编译发生在「亮出来的那一帧」,
143
+ * 所以逐帧限流必须闸在点亮这一下(`MktMesh.tickProgressiveReveal`),闸在挂载那一下拦不住 GPU 侧的账。
144
+ */
145
+ private _loaded = false;
140
146
  private _disposed = false;
141
147
 
142
148
  constructor(name?: string) {
@@ -164,6 +170,8 @@ export class ModelSlot extends Group {
164
170
  get naturalSize(): { x: number; y: number; z: number } | undefined { return this._naturalSize; }
165
171
  /** 是否已经挂上真模型(false = 仍是占位小立方体) */
166
172
  get hasModel(): boolean { return this._model !== null; }
173
+ /** 这一颗的内容落定没有 —— 库的逐帧点亮闸只认这一条(`MktMesh.tickProgressiveReveal`)。 */
174
+ get loaded(): boolean { return this._loaded; }
167
175
 
168
176
  /** 改尺寸档(米,最长边):立刻重缩内容 */
169
177
  setSize(size: number): void {
@@ -201,18 +209,31 @@ export class ModelSlot extends Group {
201
209
  async loadModel(url: string): Promise<boolean> {
202
210
  this._url = url;
203
211
  const token = ++this._loadToken;
212
+ this._loaded = false;
204
213
  this._detachModel();
205
214
  // 先立刻退回占位小立方体:GLB 在路上那段窗口(慢网/大模型)⛔ 不许让这一槽凭空消失
206
215
  // (取回失败那条支路本来也是停在占位上,两段时间里画面一致)
207
216
  this._layout();
208
- if (!url) return true;
209
- const obj = await threeLoader.loadModel(url);
217
+ if (!url) {
218
+ // 空地址 = 这一颗压根没在等什么 ⇒ 当场落定(占位小立方体就是它的最终内容)
219
+ this._loaded = true;
220
+ return true;
221
+ }
222
+ // 取回失败(404 / 解析报错都从这一档走):`threeLoader.loadModel` 是会 reject 的,⚠️ 必须当「落定在占位方块」处理
223
+ // —— 整层重建时这一颗是压着不亮的,不落定就永远隐形(旧写法里它至少还露得出占位方块)。
224
+ let obj: Object3D | undefined;
225
+ try {
226
+ obj = await threeLoader.loadModel(url);
227
+ } catch {
228
+ obj = undefined;
229
+ }
210
230
  // 迟到回调:这一槽已经改指向别的地址、或已经删除 ⇒ 当场释放,别留一份没人认领的显存
211
231
  if (this._disposed || token !== this._loadToken) {
212
232
  if (obj) disposeSubtree(obj);
213
233
  return false;
214
234
  }
215
235
  if (!obj) {
236
+ this._loaded = true;
216
237
  this._layout();
217
238
  return false;
218
239
  }
@@ -227,6 +248,7 @@ export class ModelSlot extends Group {
227
248
  this._staged = null;
228
249
  this._model = obj;
229
250
  this._contentRoot.add(obj);
251
+ this._loaded = true;
230
252
  this._layout();
231
253
  return true;
232
254
  }
@@ -257,6 +279,8 @@ export class ModelSlot extends Group {
257
279
  this._size = d.size > 0 ? d.size : MODEL_SLOT_DEFAULT_SIZE;
258
280
  this._naturalSize = d.naturalSize;
259
281
  this._url = d.url || "";
282
+ // 落定判据跟着地址走:空地址 = 没在等什么(占位方块就是最终内容);有地址 = 等 GLB 那一拍
283
+ this._loaded = !this._url;
260
284
  if (!this._url) this._detachModel();
261
285
  this._layout();
262
286
  }
@@ -16,8 +16,11 @@ import { Texture } from "three";
16
16
  * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
17
17
  */
18
18
  /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
19
- * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
20
- * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
19
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(用户决定 2026-09-25),
20
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。
21
+ * ⭐ 这一步走的是原生 `fetch`,不经过 three 的 `Loader`,所以它**自己**向 `DefaultLoadingManager`
22
+ * 报一次 `itemStart` / `itemEnd`(失败再多一次 `itemError`,与 `FileLoader` 的口径一致)——
23
+ * 否则页面拿到的加载状态只算模型和 `TextureLoader` 那些路,全景切片的下载完全不在里面。 */
21
24
  export declare function loadPanoTexture(url: string): Promise<Texture>;
22
25
  /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
23
26
  * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
@@ -1,4 +1,4 @@
1
- import { Texture } from "three";
1
+ import { DefaultLoadingManager, Texture } from "three";
2
2
  /**
3
3
  * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
4
4
  *
@@ -16,10 +16,14 @@ import { Texture } from "three";
16
16
  * 明说「已经翻过了」。1×2 像素对照实测:`<img>`+flipY=true 与 bitmap(flipY)+flipY=false 逐行一致。
17
17
  */
18
18
  /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
19
- * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
20
- * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
19
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(用户决定 2026-09-25),
20
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。
21
+ * ⭐ 这一步走的是原生 `fetch`,不经过 three 的 `Loader`,所以它**自己**向 `DefaultLoadingManager`
22
+ * 报一次 `itemStart` / `itemEnd`(失败再多一次 `itemError`,与 `FileLoader` 的口径一致)——
23
+ * 否则页面拿到的加载状态只算模型和 `TextureLoader` 那些路,全景切片的下载完全不在里面。 */
21
24
  export function loadPanoTexture(url) {
22
- return fetch(url)
25
+ DefaultLoadingManager.itemStart(url);
26
+ const task = fetch(url)
23
27
  .then((res) => {
24
28
  if (!res.ok)
25
29
  throw new Error(`HTTP ${res.status}`);
@@ -38,6 +42,12 @@ export function loadPanoTexture(url) {
38
42
  tex.userData.url = url;
39
43
  return tex;
40
44
  });
45
+ // 一条 promise 只 settle 一次 ⇒ 开始/结束各报一次,两边都不会漏;reject 原样透给调用方。
46
+ task.then(() => DefaultLoadingManager.itemEnd(url), () => {
47
+ DefaultLoadingManager.itemError(url);
48
+ DefaultLoadingManager.itemEnd(url);
49
+ });
50
+ return task;
41
51
  }
42
52
  /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
43
53
  * (不像 `<img>` 能等 GC —— 一张 270MB 的位图挂着不关,切几轮全景就把进程吃爆)。
@@ -1,4 +1,4 @@
1
- import { Texture } from "three";
1
+ import { DefaultLoadingManager, Texture } from "three";
2
2
 
3
3
  /**
4
4
  * ⭐ 甲(皇上旨 2026-10-05「你试试甲」):一张全景图(整图 / 单片瓦片,现行 32 片/全景)的**异步解码**建纹理。
@@ -18,10 +18,14 @@ import { Texture } from "three";
18
18
  */
19
19
 
20
20
  /** 发起一张全景纹理的加载(下载 + 异步解码都在浏览器工作线程,主线程只拿结果)。
21
- * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(皇上旨 2026-09-25),
22
- * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。 */
21
+ * 与 `TextureLoader.load` 同口径:**不许**把这一步排进 `threeUtils` 的全局队列(用户决定 2026-09-25),
22
+ * 排队的是「上传那一句」,见两侧的 `_commitTileToShader`。
23
+ * ⭐ 这一步走的是原生 `fetch`,不经过 three 的 `Loader`,所以它**自己**向 `DefaultLoadingManager`
24
+ * 报一次 `itemStart` / `itemEnd`(失败再多一次 `itemError`,与 `FileLoader` 的口径一致)——
25
+ * 否则页面拿到的加载状态只算模型和 `TextureLoader` 那些路,全景切片的下载完全不在里面。 */
23
26
  export function loadPanoTexture(url: string): Promise<Texture> {
24
- return fetch(url)
27
+ DefaultLoadingManager.itemStart(url);
28
+ const task = fetch(url)
25
29
  .then((res) => {
26
30
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
27
31
  return res.blob();
@@ -41,6 +45,15 @@ export function loadPanoTexture(url: string): Promise<Texture> {
41
45
  tex.userData.url = url;
42
46
  return tex;
43
47
  });
48
+ // 一条 promise 只 settle 一次 ⇒ 开始/结束各报一次,两边都不会漏;reject 原样透给调用方。
49
+ task.then(
50
+ () => DefaultLoadingManager.itemEnd(url),
51
+ () => {
52
+ DefaultLoadingManager.itemError(url);
53
+ DefaultLoadingManager.itemEnd(url);
54
+ }
55
+ );
56
+ return task;
44
57
  }
45
58
 
46
59
  /** 释放一张由 `loadPanoTexture` 建出来的纹理:`dispose()` 只退显存,**ImageBitmap 的原生内存要 `close()`**
@@ -7,8 +7,11 @@ import { type TileLayout } from "./mktShader";
7
7
  export interface PanoStitcher {
8
8
  /** 确保批次已在册并发起瓦片加载(幂等;同批已在册 ⇒ 什么都不做)。
9
9
  * 新增可选 `layout` 参数:缺省 = PANO_TILE_LAYOUT(全景 8×4 / 2 槽 / 6144×6144,2026-10-27 需求);
10
- * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。 */
11
- ensure(urls: string[], layout?: TileLayout): void;
10
+ * 图片平面传 `imageTileLayout(natW, natH)`(2×2 / 1 槽 / RT 宽高比=原图)。
11
+ * 新增可选 `front` 参数(2026-10-29 需求):`true` ⇒ 这一批**挪到在册顺序最前** = 每帧那三道工序
12
+ * 先取它的料(下载额度「每帧 2 张」先归它)。消费方每帧点一次 ⇒ 换渲染器那次整份作废后,
13
+ * 下一帧又把头名抢回来(⛔ 光靠「先入册」做不到,见 `MktMesh.prefetchHeadPanoramas` 的说明)。 */
14
+ ensure(urls: string[], layout?: TileLayout, front?: boolean): void;
12
15
  /**
13
16
  * 取某一拼合槽的纹理。**画过 ≥1 格就返回**(同日旨「需要先喂给 shader」)。
14
17
  * ⭐ 2026-10-09 皇上旨「切换全景时上一个全景的内容要渲染成深灰」:**传了 `renderer` 且该槽一格都还没画时,
@@ -74,7 +77,10 @@ export declare class PanoTileStitcher implements PanoStitcher {
74
77
  * ⚠️ 唯一按张数**能混进来**的是 10-06 那版同为 32 片的老档(切片顺序与槽分组两版一致 ⇒ 不会错位),
75
78
  * 但它的瓦片是 1024²、被画进 1536² 的格子 = 1.5× 放大 ⇒ 发虚,那类老点仍要重传一次。
76
79
  * ⚠️ 每槽数组长度由 `RT_STITCH_SLOT_COUNT` 派生(⛔ 不许手写 `[null,null]` —— 槽数再变会漏改)。 */
77
- ensure(urls: string[], layout?: TileLayout): void;
80
+ ensure(urls: string[], layout?: TileLayout, front?: boolean): void;
81
+ /** 把一批挪到在册顺序的**最前**(只动 Map 的顺序,⛔ 不动组里任何东西:RT、已画格子、瓦片账本原样)。
82
+ * 已在最前 ⇒ 一次 `keys().next()` 就返回(每帧点名走的就是这条零成本支路)。 */
83
+ private _toFront;
78
84
  /** 取某一拼合槽的纹理:**画过 ≥1 格就交出去**(同日旨「需要先喂给 shader」⇒ 不再等 `cellsPerSlot` 格画满)。
79
85
  * RT 建好时整张涂过缺失灰 ⇒ 交出去的那张里**没有未定义像素**,观感 = 该象限逐格显影。 */
80
86
  peek(batch: string, slot: number, renderer?: WebGLRenderer | null): Texture | null;