@d5techs/3dgs-lib 1.3.0 → 1.4.0

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/dist/3dgs-lib.js CHANGED
@@ -11620,6 +11620,8 @@ class TransformGizmo {
11620
11620
  __publicField(this, "dragMode", "selected");
11621
11621
  // 平面翻转
11622
11622
  __publicField(this, "flipPlanes", true);
11623
+ /** 强制等比缩放:开启后无论拖哪个轴,缩放都作用于 xyz */
11624
+ __publicField(this, "uniformScaleOnly", false);
11623
11625
  // 目标对象
11624
11626
  __publicField(this, "_target", null);
11625
11627
  // 形状
@@ -12706,7 +12708,8 @@ class TransformGizmo {
12706
12708
  }
12707
12709
  _applyScale(delta) {
12708
12710
  if (!this._target || !this._dragStartTransform) return;
12709
- const axis = this._selectedAxis;
12711
+ let axis = this._selectedAxis;
12712
+ if (this.uniformScaleOnly) axis = "xyz";
12710
12713
  let scaleFactor = 1;
12711
12714
  if (axis === "x") scaleFactor = 1 + delta.x;
12712
12715
  else if (axis === "y") scaleFactor = 1 + delta.y;
@@ -12885,6 +12888,9 @@ class GizmoManager {
12885
12888
  setGizmoMode(mode) {
12886
12889
  this.transformGizmo.mode = mode;
12887
12890
  }
12891
+ setUniformScaleOnly(enabled) {
12892
+ this.transformGizmo.uniformScaleOnly = enabled;
12893
+ }
12888
12894
  /**
12889
12895
  * 设置 Gizmo 目标对象
12890
12896
  */
@@ -13005,9 +13011,17 @@ class HotspotManager {
13005
13011
  __publicField(this, "indicatorBaseRadius", 0.02);
13006
13012
  // 搜索邻域的屏幕空间半径(像素)
13007
13013
  __publicField(this, "pickRadiusPx", 20);
13014
+ // 标签 overlay 容器
13015
+ __publicField(this, "labelContainer", null);
13016
+ // 热点点击回调
13017
+ __publicField(this, "onHotspotClicked", null);
13018
+ // 热点点击命中半径(屏幕空间 NDC 距离)
13019
+ __publicField(this, "hotspotHitRadius", 0.05);
13008
13020
  // 回调
13009
13021
  __publicField(this, "onHotspotPlaced", null);
13010
13022
  __publicField(this, "onModeChanged", null);
13023
+ __publicField(this, "_clickListenerBound", false);
13024
+ __publicField(this, "_boundOnCanvasClick", null);
13011
13025
  // ============================================
13012
13026
  // 事件处理
13013
13027
  // ============================================
@@ -13029,6 +13043,7 @@ class HotspotManager {
13029
13043
  this.boundOnMouseDown = this.onMouseDown.bind(this);
13030
13044
  this.boundOnMouseUp = this.onMouseUp.bind(this);
13031
13045
  this.boundOnKeyDown = this.onKeyDown.bind(this);
13046
+ this.initLabelContainer();
13032
13047
  }
13033
13048
  setGSRenderer(gsRenderer) {
13034
13049
  this.gsRenderer = gsRenderer;
@@ -13094,6 +13109,175 @@ class HotspotManager {
13094
13109
  setOnModeChanged(cb) {
13095
13110
  this.onModeChanged = cb;
13096
13111
  }
13112
+ // ============================================
13113
+ // 标签管理
13114
+ // ============================================
13115
+ initLabelContainer() {
13116
+ const container = document.createElement("div");
13117
+ container.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:10";
13118
+ const parent = this.canvas.parentElement;
13119
+ if (parent) {
13120
+ const pos = getComputedStyle(parent).position;
13121
+ if (pos === "static") parent.style.position = "relative";
13122
+ parent.appendChild(container);
13123
+ }
13124
+ this.labelContainer = container;
13125
+ }
13126
+ /**
13127
+ * 设置热点标签
13128
+ */
13129
+ setHotspotLabel(hotspotIndex, text, position = "top", fontSize = 14, visible = true) {
13130
+ if (hotspotIndex < 0 || hotspotIndex >= this.hotspots.length) return false;
13131
+ const info = this.hotspots[hotspotIndex];
13132
+ info.label = { text, position, fontSize, visible };
13133
+ this.ensureLabelElement(info);
13134
+ this.applyLabelStyle(info);
13135
+ return true;
13136
+ }
13137
+ /**
13138
+ * 设置热点标签可见性
13139
+ */
13140
+ setHotspotLabelVisible(hotspotIndex, visible) {
13141
+ if (hotspotIndex < 0 || hotspotIndex >= this.hotspots.length) return false;
13142
+ const info = this.hotspots[hotspotIndex];
13143
+ if (!info.label) return false;
13144
+ info.label.visible = visible;
13145
+ if (info._labelElement) {
13146
+ info._labelElement.style.display = visible ? "block" : "none";
13147
+ }
13148
+ return true;
13149
+ }
13150
+ /**
13151
+ * 获取热点标签配置
13152
+ */
13153
+ getHotspotLabel(hotspotIndex) {
13154
+ if (hotspotIndex < 0 || hotspotIndex >= this.hotspots.length) return null;
13155
+ return this.hotspots[hotspotIndex].label ?? null;
13156
+ }
13157
+ ensureLabelElement(info) {
13158
+ if (info._labelElement) return;
13159
+ if (!this.labelContainer) return;
13160
+ const el = document.createElement("div");
13161
+ el.style.cssText = "position:absolute;white-space:nowrap;color:#fff;text-shadow:0 1px 4px rgba(0,0,0,0.8);pointer-events:none;transform-origin:center;transition:opacity 0.15s";
13162
+ this.labelContainer.appendChild(el);
13163
+ info._labelElement = el;
13164
+ }
13165
+ applyLabelStyle(info) {
13166
+ const el = info._labelElement;
13167
+ const label = info.label;
13168
+ if (!el || !label) return;
13169
+ el.textContent = label.text;
13170
+ el.style.fontSize = `${label.fontSize}px`;
13171
+ el.style.display = label.visible ? "block" : "none";
13172
+ }
13173
+ /**
13174
+ * 每帧更新所有标签的屏幕位置
13175
+ */
13176
+ updateLabels() {
13177
+ var _a2;
13178
+ if (!this.labelContainer) return;
13179
+ const rect = this.canvas.getBoundingClientRect();
13180
+ const vp = this.camera.viewProjectionMatrix;
13181
+ for (const info of this.hotspots) {
13182
+ if (!((_a2 = info.label) == null ? void 0 : _a2.visible) || !info._labelElement) continue;
13183
+ const [px, py, pz] = info.position;
13184
+ const clipW = vp[3] * px + vp[7] * py + vp[11] * pz + vp[15];
13185
+ if (clipW <= 0) {
13186
+ info._labelElement.style.opacity = "0";
13187
+ continue;
13188
+ }
13189
+ const ndcX = (vp[0] * px + vp[4] * py + vp[8] * pz + vp[12]) / clipW;
13190
+ const ndcY = (vp[1] * px + vp[5] * py + vp[9] * pz + vp[13]) / clipW;
13191
+ const screenX = (ndcX + 1) * 0.5 * rect.width;
13192
+ const screenY = (1 - ndcY) * 0.5 * rect.height;
13193
+ const el = info._labelElement;
13194
+ el.style.opacity = "1";
13195
+ const iconOffset = 20;
13196
+ switch (info.label.position) {
13197
+ case "top":
13198
+ el.style.left = `${screenX}px`;
13199
+ el.style.top = `${screenY - iconOffset}px`;
13200
+ el.style.transform = "translate(-50%, -100%)";
13201
+ break;
13202
+ case "left":
13203
+ el.style.left = `${screenX - iconOffset}px`;
13204
+ el.style.top = `${screenY}px`;
13205
+ el.style.transform = "translate(-100%, -50%)";
13206
+ break;
13207
+ case "right":
13208
+ el.style.left = `${screenX + iconOffset}px`;
13209
+ el.style.top = `${screenY}px`;
13210
+ el.style.transform = "translate(0, -50%)";
13211
+ break;
13212
+ }
13213
+ }
13214
+ }
13215
+ // ============================================
13216
+ // 热点点击检测
13217
+ // ============================================
13218
+ /**
13219
+ * 设置热点点击回调
13220
+ */
13221
+ setOnHotspotClicked(cb) {
13222
+ this.onHotspotClicked = cb;
13223
+ if (cb && !this._clickListenerBound) {
13224
+ this._boundOnCanvasClick = this._onCanvasClick.bind(this);
13225
+ this.canvas.addEventListener("click", this._boundOnCanvasClick);
13226
+ this._clickListenerBound = true;
13227
+ } else if (!cb && this._clickListenerBound && this._boundOnCanvasClick) {
13228
+ this.canvas.removeEventListener("click", this._boundOnCanvasClick);
13229
+ this._clickListenerBound = false;
13230
+ }
13231
+ }
13232
+ _onCanvasClick(e) {
13233
+ if (this.active) return;
13234
+ const idx = this.hitTestHotspot(e.clientX, e.clientY);
13235
+ if (idx >= 0 && this.onHotspotClicked) {
13236
+ this.onHotspotClicked(idx, this.hotspots[idx]);
13237
+ }
13238
+ }
13239
+ /**
13240
+ * 检测给定屏幕坐标命中了哪个热点
13241
+ * @returns 热点索引,-1 表示未命中
13242
+ */
13243
+ hitTestHotspot(clientX, clientY) {
13244
+ if (this.hotspots.length === 0) return -1;
13245
+ const rect = this.canvas.getBoundingClientRect();
13246
+ const ndcX = (clientX - rect.left) / rect.width * 2 - 1;
13247
+ const ndcY = -((clientY - rect.top) / rect.height * 2 - 1);
13248
+ const vp = this.camera.viewProjectionMatrix;
13249
+ const camPos = this.camera.position;
13250
+ let closestDist = Infinity;
13251
+ let closestIdx = -1;
13252
+ for (let i = 0; i < this.hotspots.length; i++) {
13253
+ const h = this.hotspots[i];
13254
+ const [px, py, pz] = h.position;
13255
+ const clipW = vp[3] * px + vp[7] * py + vp[11] * pz + vp[15];
13256
+ if (clipW <= 0) continue;
13257
+ const clipX = (vp[0] * px + vp[4] * py + vp[8] * pz + vp[12]) / clipW;
13258
+ const clipY = (vp[1] * px + vp[5] * py + vp[9] * pz + vp[13]) / clipW;
13259
+ const dx = clipX - ndcX;
13260
+ const dy = clipY - ndcY;
13261
+ const screenDist = Math.sqrt(dx * dx + dy * dy);
13262
+ if (screenDist < this.hotspotHitRadius) {
13263
+ const cx = px - camPos[0];
13264
+ const cy = py - camPos[1];
13265
+ const cz = pz - camPos[2];
13266
+ const dist3D = cx * cx + cy * cy + cz * cz;
13267
+ if (dist3D < closestDist) {
13268
+ closestDist = dist3D;
13269
+ closestIdx = i;
13270
+ }
13271
+ }
13272
+ }
13273
+ return closestIdx;
13274
+ }
13275
+ /**
13276
+ * 设置热点点击命中半径(NDC 空间,默认 0.05)
13277
+ */
13278
+ setHotspotHitRadius(radius) {
13279
+ this.hotspotHitRadius = radius;
13280
+ }
13097
13281
  /**
13098
13282
  * Returns the current pick pixel coordinates in device pixels.
13099
13283
  * Called by the render loop to pass to prepareDepthNormalPass.
@@ -13735,6 +13919,117 @@ class HotspotManager {
13735
13919
  (h) => h.meshStartIndex === overlayMeshStartIndex
13736
13920
  );
13737
13921
  }
13922
+ /**
13923
+ * 在指定位置和法线方向程序化放置一个热点(用于从保存数据恢复)。
13924
+ * 逻辑与交互放置一致:加载 OBJ → 计算朝向矩阵 → addOverlayMesh → 注册到内部列表。
13925
+ * @returns 放置后的 HotspotInfo,失败返回 null
13926
+ */
13927
+ async placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale) {
13928
+ var _a2;
13929
+ try {
13930
+ const loadedMeshes = await this.objLoader.load(objUrl);
13931
+ if (loadedMeshes.length === 0) return null;
13932
+ if (visualDiameter == null) {
13933
+ const bbox = (_a2 = this.gsRenderer) == null ? void 0 : _a2.getBoundingBox();
13934
+ visualDiameter = bbox ? bbox.radius * 0.03 : 0.3;
13935
+ }
13936
+ if (normalOffset == null) {
13937
+ normalOffset = 0.02;
13938
+ }
13939
+ const meshStartIndex = this.meshRenderer.getOverlayMeshCount();
13940
+ const [nx, ny, nz] = normal;
13941
+ let upX = 0, upY = 1, upZ = 0;
13942
+ if (Math.abs(ny) > 0.99) {
13943
+ upX = 1;
13944
+ upY = 0;
13945
+ upZ = 0;
13946
+ }
13947
+ let rx = upY * nz - upZ * ny;
13948
+ let ry = upZ * nx - upX * nz;
13949
+ let rz = upX * ny - upY * nx;
13950
+ const rLen = Math.sqrt(rx * rx + ry * ry + rz * rz) || 1;
13951
+ rx /= rLen;
13952
+ ry /= rLen;
13953
+ rz /= rLen;
13954
+ let fx = ny * rz - nz * ry;
13955
+ let fy = nz * rx - nx * rz;
13956
+ let fz = nx * ry - ny * rx;
13957
+ const fLen = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1;
13958
+ fx /= fLen;
13959
+ fy /= fLen;
13960
+ fz /= fLen;
13961
+ let placedScale = 1;
13962
+ let placedLocalCenter = [0, 0, 0];
13963
+ const firstBbox = loadedMeshes[0].mesh.getLocalBoundingBox();
13964
+ if (firstBbox) {
13965
+ const modelMaxDim = Math.max(
13966
+ firstBbox.max[0] - firstBbox.min[0],
13967
+ firstBbox.max[1] - firstBbox.min[1],
13968
+ firstBbox.max[2] - firstBbox.min[2],
13969
+ 1e-6
13970
+ );
13971
+ placedScale = overrideScale != null ? overrideScale : visualDiameter / modelMaxDim;
13972
+ placedLocalCenter = [
13973
+ (firstBbox.min[0] + firstBbox.max[0]) / 2,
13974
+ (firstBbox.min[1] + firstBbox.max[1]) / 2,
13975
+ (firstBbox.min[2] + firstBbox.max[2]) / 2
13976
+ ];
13977
+ }
13978
+ for (const { mesh, material } of loadedMeshes) {
13979
+ const bbox = mesh.getLocalBoundingBox();
13980
+ let s = placedScale;
13981
+ let lcx = 0, lcy = 0, lcz = 0;
13982
+ if (bbox) {
13983
+ if (overrideScale != null) {
13984
+ s = overrideScale;
13985
+ } else {
13986
+ const mDim = Math.max(bbox.max[0] - bbox.min[0], bbox.max[1] - bbox.min[1], bbox.max[2] - bbox.min[2], 1e-6);
13987
+ s = visualDiameter / mDim;
13988
+ }
13989
+ lcx = (bbox.min[0] + bbox.max[0]) / 2;
13990
+ lcy = (bbox.min[1] + bbox.max[1]) / 2;
13991
+ lcz = (bbox.min[2] + bbox.max[2]) / 2;
13992
+ }
13993
+ const owx = (rx * lcx + fx * lcy + nx * lcz) * s;
13994
+ const owy = (ry * lcx + fy * lcy + ny * lcz) * s;
13995
+ const owz = (rz * lcx + fz * lcy + nz * lcz) * s;
13996
+ const m = mesh.modelMatrix;
13997
+ m[0] = rx * s;
13998
+ m[1] = ry * s;
13999
+ m[2] = rz * s;
14000
+ m[3] = 0;
14001
+ m[4] = fx * s;
14002
+ m[5] = fy * s;
14003
+ m[6] = fz * s;
14004
+ m[7] = 0;
14005
+ m[8] = nx * s;
14006
+ m[9] = ny * s;
14007
+ m[10] = nz * s;
14008
+ m[11] = 0;
14009
+ m[12] = position[0] + nx * normalOffset - owx;
14010
+ m[13] = position[1] + ny * normalOffset - owy;
14011
+ m[14] = position[2] + nz * normalOffset - owz;
14012
+ m[15] = 1;
14013
+ this.decomposeModelMatrix(mesh);
14014
+ this.meshRenderer.addOverlayMesh(mesh, material);
14015
+ }
14016
+ const info = {
14017
+ position: [...position],
14018
+ normal: [...normal],
14019
+ meshStartIndex,
14020
+ meshCount: loadedMeshes.length,
14021
+ billboard: false,
14022
+ placedScale,
14023
+ placedNormalOffset: normalOffset,
14024
+ placedLocalCenter
14025
+ };
14026
+ this.hotspots.push(info);
14027
+ return info;
14028
+ } catch (error) {
14029
+ console.error("HotspotManager: placeHotspotAt 失败:", error);
14030
+ return null;
14031
+ }
14032
+ }
13738
14033
  /**
13739
14034
  * 每帧调用:更新所有 billboard 热点朝向相机
13740
14035
  */
@@ -13818,6 +14113,7 @@ class HotspotManager {
13818
14113
  this.decomposeModelMatrix(mesh);
13819
14114
  }
13820
14115
  }
14116
+ this.updateLabels();
13821
14117
  }
13822
14118
  /**
13823
14119
  * 关闭 billboard 时恢复到放置时的法线朝向
@@ -13895,7 +14191,18 @@ class HotspotManager {
13895
14191
  // 生命周期
13896
14192
  // ============================================
13897
14193
  destroy() {
14194
+ var _a2, _b2;
13898
14195
  this.exit();
14196
+ if (this._clickListenerBound && this._boundOnCanvasClick) {
14197
+ this.canvas.removeEventListener("click", this._boundOnCanvasClick);
14198
+ this._clickListenerBound = false;
14199
+ }
14200
+ for (const info of this.hotspots) {
14201
+ (_a2 = info._labelElement) == null ? void 0 : _a2.remove();
14202
+ info._labelElement = void 0;
14203
+ }
14204
+ (_b2 = this.labelContainer) == null ? void 0 : _b2.remove();
14205
+ this.labelContainer = null;
13899
14206
  if (this.indicatorMesh && !this.indicatorAdded) {
13900
14207
  this.indicatorMesh.destroy();
13901
14208
  }
@@ -14055,8 +14362,9 @@ class App {
14055
14362
  }
14056
14363
  /**
14057
14364
  * 加载 Splat 文件
14365
+ * @param coordinateSystem 源数据坐标系,默认 'blender'(Z-up → Y-up 自动转换)
14058
14366
  */
14059
- async addSplat(urlOrBuffer, onProgress, isLocalFile = false) {
14367
+ async addSplat(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
14060
14368
  try {
14061
14369
  let buffer;
14062
14370
  if (typeof urlOrBuffer === "string") {
@@ -14073,6 +14381,16 @@ class App {
14073
14381
  }
14074
14382
  if (onProgress) onProgress(50, "parse");
14075
14383
  const splats = deserializeSplat(buffer);
14384
+ if (coordinateSystem === "blender") {
14385
+ for (const s of splats) {
14386
+ const [mx, my, mz] = s.mean;
14387
+ s.mean = [mx, mz, my];
14388
+ const [sx, sy, sz] = s.scale;
14389
+ s.scale = [sx, sz, sy];
14390
+ const [rw, rx, ry, rz] = s.rotation;
14391
+ s.rotation = [-rw, rx, rz, ry];
14392
+ }
14393
+ }
14076
14394
  if (onProgress) onProgress(90, "parse");
14077
14395
  if (onProgress) onProgress(90, "upload");
14078
14396
  const gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
@@ -14088,8 +14406,9 @@ class App {
14088
14406
  }
14089
14407
  /**
14090
14408
  * 加载 SOG 文件 (Spatially Ordered Gaussians)
14409
+ * @param coordinateSystem 源数据坐标系,默认 'blender'(Z-up → Y-up 自动转换)
14091
14410
  */
14092
- async addSOG(urlOrBuffer, onProgress, isLocalFile = false) {
14411
+ async addSOG(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
14093
14412
  try {
14094
14413
  const isMobile = isMobileDevice();
14095
14414
  let buffer;
@@ -14111,6 +14430,22 @@ class App {
14111
14430
  onProgress(50 + p / 100 * 40, stage);
14112
14431
  }
14113
14432
  });
14433
+ if (coordinateSystem === "blender") {
14434
+ const { positions, scales, rotations, count } = compactData;
14435
+ for (let i = 0; i < count; i++) {
14436
+ const i3 = i * 3, i4 = i * 4;
14437
+ const py = positions[i3 + 1], pz = positions[i3 + 2];
14438
+ positions[i3 + 1] = pz;
14439
+ positions[i3 + 2] = py;
14440
+ const sy = scales[i3 + 1], sz = scales[i3 + 2];
14441
+ scales[i3 + 1] = sz;
14442
+ scales[i3 + 2] = sy;
14443
+ const rw = rotations[i4], ry = rotations[i4 + 2], rz = rotations[i4 + 3];
14444
+ rotations[i4] = -rw;
14445
+ rotations[i4 + 2] = rz;
14446
+ rotations[i4 + 3] = ry;
14447
+ }
14448
+ }
14114
14449
  if (onProgress) onProgress(90, "upload");
14115
14450
  let gsRenderer;
14116
14451
  if (isMobile) {
@@ -14315,6 +14650,9 @@ class App {
14315
14650
  setGizmoMode(mode) {
14316
14651
  this.gizmoManager.setGizmoMode(mode);
14317
14652
  }
14653
+ setUniformScaleOnly(enabled) {
14654
+ this.gizmoManager.setUniformScaleOnly(enabled);
14655
+ }
14318
14656
  setGizmoTarget(object) {
14319
14657
  this.gizmoManager.setGizmoTarget(object);
14320
14658
  }
@@ -14413,10 +14751,53 @@ class App {
14413
14751
  findHotspotIndexByMeshStart(overlayMeshStartIndex) {
14414
14752
  return this.hotspotManager.findHotspotIndexByMeshStart(overlayMeshStartIndex);
14415
14753
  }
14754
+ async placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale) {
14755
+ return this.hotspotManager.placeHotspotAt(objUrl, position, normal, visualDiameter, normalOffset, overrideScale);
14756
+ }
14416
14757
  getOverlayMeshByIndex(index) {
14417
14758
  return this.sceneManager.getOverlayMeshByIndex(index);
14418
14759
  }
14419
14760
  // ============================================
14761
+ // 热点点击 & 标签
14762
+ // ============================================
14763
+ /**
14764
+ * 检测给定屏幕坐标命中了哪个热点
14765
+ * @returns 热点索引,-1 表示未命中
14766
+ */
14767
+ hitTestHotspot(clientX, clientY) {
14768
+ return this.hotspotManager.hitTestHotspot(clientX, clientY);
14769
+ }
14770
+ /**
14771
+ * 设置热点点击回调(非放置模式下生效)
14772
+ */
14773
+ setOnHotspotClicked(cb) {
14774
+ this.hotspotManager.setOnHotspotClicked(cb);
14775
+ }
14776
+ /**
14777
+ * 设置热点点击命中半径(NDC 空间,默认 0.05)
14778
+ */
14779
+ setHotspotHitRadius(radius) {
14780
+ this.hotspotManager.setHotspotHitRadius(radius);
14781
+ }
14782
+ /**
14783
+ * 设置热点文字标签
14784
+ */
14785
+ setHotspotLabel(hotspotIndex, text, position, fontSize, visible) {
14786
+ return this.hotspotManager.setHotspotLabel(hotspotIndex, text, position, fontSize, visible);
14787
+ }
14788
+ /**
14789
+ * 设置热点标签可见性
14790
+ */
14791
+ setHotspotLabelVisible(hotspotIndex, visible) {
14792
+ return this.hotspotManager.setHotspotLabelVisible(hotspotIndex, visible);
14793
+ }
14794
+ /**
14795
+ * 获取热点标签配置
14796
+ */
14797
+ getHotspotLabel(hotspotIndex) {
14798
+ return this.hotspotManager.getHotspotLabel(hotspotIndex);
14799
+ }
14800
+ // ============================================
14420
14801
  // 内部方法
14421
14802
  // ============================================
14422
14803
  async fetchWithProgress(url, onProgress) {