@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 +384 -3
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +384 -3
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +32 -3
- package/dist/core/gizmo/TransformGizmo.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/interaction/GizmoManager.d.ts +1 -0
- package/dist/interaction/HotspotManager.d.ts +57 -0
- package/package.json +1 -1
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
|
-
|
|
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) {
|