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