@sequent-org/ifc-viewer 1.2.4-ci.46.0 → 1.2.4-ci.47.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/README.md +11 -1
- package/package.json +1 -1
- package/src/IfcViewer.js +154 -28
- package/src/ifc/IfcService.js +0 -2
- package/src/index.js +10 -0
- package/src/main.js +122 -33
- package/src/model-loading/ModelLoaderRegistry.js +252 -0
- package/src/model-loading/loaders/DaeModelLoader.js +275 -0
- package/src/model-loading/loaders/FbxModelLoader.js +68 -0
- package/src/model-loading/loaders/GltfModelLoader.js +316 -0
- package/src/model-loading/loaders/IfcModelLoader.js +77 -0
- package/src/model-loading/loaders/ObjModelLoader.js +310 -0
- package/src/model-loading/loaders/StlModelLoader.js +102 -0
- package/src/model-loading/loaders/TdsModelLoader.js +205 -0
- package/src/viewer/Viewer.js +271 -37
package/src/viewer/Viewer.js
CHANGED
|
@@ -162,6 +162,8 @@ export class Viewer {
|
|
|
162
162
|
this._home = {
|
|
163
163
|
cameraPos: null,
|
|
164
164
|
target: new THREE.Vector3(0, 0, 0),
|
|
165
|
+
// FOV для перспективы (часть "масштаба" кадра)
|
|
166
|
+
perspFov: null,
|
|
165
167
|
edgesVisible: false,
|
|
166
168
|
flatShading: true,
|
|
167
169
|
quality: 'medium',
|
|
@@ -249,7 +251,7 @@ export class Viewer {
|
|
|
249
251
|
|
|
250
252
|
/**
|
|
251
253
|
* Возвращает "домашнюю" точку вращения для текущей модели:
|
|
252
|
-
* центр bbox
|
|
254
|
+
* центр bbox (совпадает с кадрированием при первичной загрузке).
|
|
253
255
|
* @returns {THREE.Vector3|null}
|
|
254
256
|
*/
|
|
255
257
|
#getDefaultPivotForActiveModel() {
|
|
@@ -258,12 +260,7 @@ export class Viewer {
|
|
|
258
260
|
try {
|
|
259
261
|
const box = new THREE.Box3().setFromObject(subject);
|
|
260
262
|
const center = box.getCenter(new THREE.Vector3());
|
|
261
|
-
|
|
262
|
-
const pivot = center.clone();
|
|
263
|
-
// Держим модель "выше" в кадре, как при replaceWithModel()
|
|
264
|
-
const verticalBias = size.y * 0.30; // 30% высоты
|
|
265
|
-
pivot.y = center.y - verticalBias;
|
|
266
|
-
return pivot;
|
|
263
|
+
return center.clone();
|
|
267
264
|
} catch (_) {
|
|
268
265
|
return null;
|
|
269
266
|
}
|
|
@@ -272,7 +269,7 @@ export class Viewer {
|
|
|
272
269
|
/**
|
|
273
270
|
* Возвращает целевой pivot для ЛКМ-вращения:
|
|
274
271
|
* - если модель двигали ПКМ, используем фиксированную ось (pivotAnchor)
|
|
275
|
-
* - иначе используем "домашний" pivot (центр bbox
|
|
272
|
+
* - иначе используем "домашний" pivot (центр bbox)
|
|
276
273
|
* @returns {THREE.Vector3|null}
|
|
277
274
|
*/
|
|
278
275
|
#getDesiredPivotForRotate() {
|
|
@@ -665,7 +662,8 @@ export class Viewer {
|
|
|
665
662
|
sizePx: 96,
|
|
666
663
|
marginPx: 10,
|
|
667
664
|
opacity: 0.6,
|
|
668
|
-
|
|
665
|
+
// Home-кнопка NavCube: возвращаем ТОЛЬКО камеру (не трогаем инструменты/сечения/стили)
|
|
666
|
+
onHome: () => this.goHomeViewOnly(),
|
|
669
667
|
});
|
|
670
668
|
|
|
671
669
|
// Визуальная ось вращения: события мыши и контролов
|
|
@@ -740,6 +738,7 @@ export class Viewer {
|
|
|
740
738
|
// Сохраним Home-снапшот после инициализации
|
|
741
739
|
this._home.cameraPos = this.camera.position.clone();
|
|
742
740
|
this._home.target = this.controls.target.clone();
|
|
741
|
+
this._home.perspFov = (this.camera && this.camera.isPerspectiveCamera) ? this.camera.fov : (this._projection?.persp?.fov ?? 20);
|
|
743
742
|
this._home.edgesVisible = this.edgesVisible;
|
|
744
743
|
this._home.flatShading = this.flatShading;
|
|
745
744
|
this._home.quality = this.quality;
|
|
@@ -1032,6 +1031,160 @@ export class Viewer {
|
|
|
1032
1031
|
}
|
|
1033
1032
|
}
|
|
1034
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Кадрирует объект так, чтобы он гарантированно помещался в "безопасной области" кадра.
|
|
1036
|
+
* Идея безопасной области задаётся сеткой: например 4×4, и объект должен занимать
|
|
1037
|
+
* не больше spanCols×spanRows в центре (по умолчанию 2×2, т.е. ~50% по ширине/высоте).
|
|
1038
|
+
*
|
|
1039
|
+
* - Для Perspective: подбираем дистанцию через bbox и текущий aspect + FOV.
|
|
1040
|
+
* - Для Ortho: подбираем zoom (и при необходимости расширяем orthoHalfHeight), чтобы bbox поместился.
|
|
1041
|
+
*
|
|
1042
|
+
* @param {THREE.Object3D} object3D
|
|
1043
|
+
* @param {Object} [opts]
|
|
1044
|
+
* @param {number} [opts.gridCols=4]
|
|
1045
|
+
* @param {number} [opts.gridRows=4]
|
|
1046
|
+
* @param {number} [opts.spanCols=2]
|
|
1047
|
+
* @param {number} [opts.spanRows=2]
|
|
1048
|
+
* @param {number} [opts.extraPadding=1.05] - доп. запас поверх математического fit (>=1)
|
|
1049
|
+
* @param {number} [opts.perspectiveFov=20] - целевой FOV для perspective
|
|
1050
|
+
* @param {THREE.Vector3} [opts.viewDir] - направление от цели к камере (front-right-top)
|
|
1051
|
+
* @param {boolean} [opts.log=false]
|
|
1052
|
+
* @returns {{center:THREE.Vector3,size:THREE.Vector3,padding:number,mode:string}|null}
|
|
1053
|
+
*/
|
|
1054
|
+
frameObjectToViewportGrid(object3D, opts = {}) {
|
|
1055
|
+
if (!object3D || !this.camera || !this.controls) return null;
|
|
1056
|
+
|
|
1057
|
+
const {
|
|
1058
|
+
gridCols = 4,
|
|
1059
|
+
gridRows = 4,
|
|
1060
|
+
spanCols = 2,
|
|
1061
|
+
spanRows = 2,
|
|
1062
|
+
extraPadding = 1.05,
|
|
1063
|
+
perspectiveFov = 20,
|
|
1064
|
+
viewDir = null,
|
|
1065
|
+
log = false,
|
|
1066
|
+
} = opts || {};
|
|
1067
|
+
|
|
1068
|
+
const box = new THREE.Box3().setFromObject(object3D);
|
|
1069
|
+
const size = box.getSize(new THREE.Vector3());
|
|
1070
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1071
|
+
|
|
1072
|
+
const safeRX = Math.max(1e-6, Math.min(1, Number(spanCols) / Math.max(1, Number(gridCols))));
|
|
1073
|
+
const safeRY = Math.max(1e-6, Math.min(1, Number(spanRows) / Math.max(1, Number(gridRows))));
|
|
1074
|
+
const safeR = Math.max(1e-6, Math.min(safeRX, safeRY));
|
|
1075
|
+
const padding = Math.max(1.0, (1 / safeR) * Math.max(1.0, Number(extraPadding) || 1.0));
|
|
1076
|
+
|
|
1077
|
+
// Направление вида: по умолчанию "front-right-top" в текущей сцене.
|
|
1078
|
+
// Важно: в некоторых IFC/сценах "front/right" может не совпадать со знаками мировых осей,
|
|
1079
|
+
// поэтому вектор легко переопределяется через opts.viewDir.
|
|
1080
|
+
let dir = null;
|
|
1081
|
+
if (viewDir && viewDir.isVector3) dir = viewDir.clone();
|
|
1082
|
+
else dir = new THREE.Vector3(-1, 0.6, 1);
|
|
1083
|
+
const dirLen = dir.length();
|
|
1084
|
+
if (dirLen > 1e-6) dir.multiplyScalar(1 / dirLen);
|
|
1085
|
+
else dir.set(0, 0.2, 1).normalize();
|
|
1086
|
+
const dirN = dir.clone(); // нормализованный (для логов и вычислений без мутаций)
|
|
1087
|
+
|
|
1088
|
+
// Строго по центру bbox
|
|
1089
|
+
this.controls.target.copy(center);
|
|
1090
|
+
|
|
1091
|
+
const aspect = this._getAspect?.() || (this.camera.aspect || 1);
|
|
1092
|
+
|
|
1093
|
+
if (this.camera.isPerspectiveCamera) {
|
|
1094
|
+
const fov = Number.isFinite(Number(perspectiveFov)) ? Number(perspectiveFov) : this.camera.fov;
|
|
1095
|
+
if (Number.isFinite(fov) && fov > 1e-3 && fov < 179) {
|
|
1096
|
+
this.camera.fov = fov;
|
|
1097
|
+
this.camera.updateProjectionMatrix();
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
if (this.camera.aspect !== aspect) {
|
|
1101
|
+
this.camera.aspect = aspect;
|
|
1102
|
+
this.camera.updateProjectionMatrix();
|
|
1103
|
+
}
|
|
1104
|
+
} catch (_) {}
|
|
1105
|
+
|
|
1106
|
+
const dist = this.#computeFitDistanceForSize(size, padding);
|
|
1107
|
+
this.camera.position.copy(center.clone().add(dirN.clone().multiplyScalar(dist)));
|
|
1108
|
+
this.camera.updateProjectionMatrix();
|
|
1109
|
+
this.controls.update();
|
|
1110
|
+
|
|
1111
|
+
if (log) {
|
|
1112
|
+
// eslint-disable-next-line no-console
|
|
1113
|
+
console.log('[Viewer] frameObjectToViewportGrid(persp)', {
|
|
1114
|
+
grid: { gridCols, gridRows, spanCols, spanRows, safeRX, safeRY, padding },
|
|
1115
|
+
bbox: {
|
|
1116
|
+
size: { x: +size.x.toFixed(3), y: +size.y.toFixed(3), z: +size.z.toFixed(3) },
|
|
1117
|
+
center: { x: +center.x.toFixed(3), y: +center.y.toFixed(3), z: +center.z.toFixed(3) },
|
|
1118
|
+
},
|
|
1119
|
+
camera: { fov: this.camera.fov, aspect: this.camera.aspect, dist: +dist.toFixed(3) },
|
|
1120
|
+
viewDir: { x: +dirN.x.toFixed(3), y: +dirN.y.toFixed(3), z: +dirN.z.toFixed(3) },
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return { center, size, padding, mode: 'perspective' };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (this.camera.isOrthographicCamera) {
|
|
1128
|
+
const safeSizeX = Math.max(1e-6, size.x);
|
|
1129
|
+
const safeSizeY = Math.max(1e-6, size.y);
|
|
1130
|
+
const fitHeight = Math.max(safeSizeY, safeSizeX / Math.max(1e-6, aspect));
|
|
1131
|
+
const neededHalfVisible = (fitHeight * padding) / 2;
|
|
1132
|
+
|
|
1133
|
+
let halfH = this._projection?.orthoHalfHeight || Math.abs(this.camera.top) || 10;
|
|
1134
|
+
halfH = Math.max(0.01, halfH);
|
|
1135
|
+
|
|
1136
|
+
const minZoom = this.controls?.minZoom ?? this._projection?.minZoom ?? 0.25;
|
|
1137
|
+
const maxZoom = this.controls?.maxZoom ?? this._projection?.maxZoom ?? 8;
|
|
1138
|
+
|
|
1139
|
+
let zoomFit = halfH / Math.max(1e-6, neededHalfVisible);
|
|
1140
|
+
|
|
1141
|
+
if (zoomFit < minZoom) {
|
|
1142
|
+
halfH = Math.max(halfH, neededHalfVisible * minZoom);
|
|
1143
|
+
try { this._projection.orthoHalfHeight = halfH; } catch (_) {}
|
|
1144
|
+
try {
|
|
1145
|
+
this.camera.left = -halfH * aspect;
|
|
1146
|
+
this.camera.right = halfH * aspect;
|
|
1147
|
+
this.camera.top = halfH;
|
|
1148
|
+
this.camera.bottom = -halfH;
|
|
1149
|
+
} catch (_) {}
|
|
1150
|
+
zoomFit = halfH / Math.max(1e-6, neededHalfVisible);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const zoom = Math.min(maxZoom, zoomFit);
|
|
1154
|
+
this.camera.zoom = Math.max(1e-6, zoom);
|
|
1155
|
+
|
|
1156
|
+
const dist = Math.max(1.0, size.length());
|
|
1157
|
+
this.camera.position.copy(center.clone().add(dirN.clone().multiplyScalar(dist)));
|
|
1158
|
+
|
|
1159
|
+
this.camera.updateProjectionMatrix();
|
|
1160
|
+
this.controls.update();
|
|
1161
|
+
|
|
1162
|
+
if (log) {
|
|
1163
|
+
// eslint-disable-next-line no-console
|
|
1164
|
+
console.log('[Viewer] frameObjectToViewportGrid(ortho)', {
|
|
1165
|
+
grid: { gridCols, gridRows, spanCols, spanRows, safeRX, safeRY, padding },
|
|
1166
|
+
bbox: {
|
|
1167
|
+
size: { x: +size.x.toFixed(3), y: +size.y.toFixed(3), z: +size.z.toFixed(3) },
|
|
1168
|
+
center: { x: +center.x.toFixed(3), y: +center.y.toFixed(3), z: +center.z.toFixed(3) },
|
|
1169
|
+
},
|
|
1170
|
+
ortho: {
|
|
1171
|
+
aspect: +aspect.toFixed(3),
|
|
1172
|
+
halfH: +halfH.toFixed(3),
|
|
1173
|
+
neededHalfVisible: +neededHalfVisible.toFixed(3),
|
|
1174
|
+
zoom: +this.camera.zoom.toFixed(3),
|
|
1175
|
+
minZoom,
|
|
1176
|
+
maxZoom,
|
|
1177
|
+
},
|
|
1178
|
+
viewDir: { x: +dirN.x.toFixed(3), y: +dirN.y.toFixed(3), z: +dirN.z.toFixed(3) },
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return { center, size, padding, mode: 'ortho' };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return { center, size, padding, mode: 'unknown' };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1035
1188
|
// Вычисляет дистанцию до объекта, при которой он полностью помещается в кадр
|
|
1036
1189
|
#computeFitDistanceForSize(size, padding = 1.2) {
|
|
1037
1190
|
// Защита от нулевых размеров
|
|
@@ -1246,48 +1399,47 @@ export class Viewer {
|
|
|
1246
1399
|
// Синхронизируем "сечение → shadow-pass" для материалов после назначения пресета
|
|
1247
1400
|
this.#applyClipShadowsToModelMaterials();
|
|
1248
1401
|
|
|
1249
|
-
// Настроим пределы зума
|
|
1250
|
-
|
|
1402
|
+
// Настроим пределы зума под габариты модели (кадрирование делаем отдельно ниже, на следующем кадре).
|
|
1403
|
+
// Здесь важно в первую очередь "оздоровить" near/far под размер модели.
|
|
1404
|
+
this.applyAdaptiveZoomLimits(object3D, { padding: 2.1, slack: 2.5, minRatio: 0.05, recenter: false });
|
|
1251
1405
|
|
|
1252
1406
|
// Если "Тест" активен, сразу применим его к только что загруженной модели (самозатенение + shadow camera по bbox)
|
|
1253
1407
|
if (this._testPreset?.enabled) {
|
|
1254
1408
|
try { this.#applyTestPresetToScene(); } catch (_) {}
|
|
1255
1409
|
}
|
|
1256
1410
|
|
|
1257
|
-
// На следующем кадре
|
|
1411
|
+
// На следующем кадре выставим кадрирование "в безопасной зоне" (2×2 из 4×4) и ракурс front-right-top.
|
|
1258
1412
|
try {
|
|
1259
|
-
const box = new THREE.Box3().setFromObject(object3D);
|
|
1260
|
-
const center = box.getCenter(new THREE.Vector3());
|
|
1261
1413
|
requestAnimationFrame(() => {
|
|
1262
1414
|
if (!this.camera || !this.controls) return;
|
|
1263
|
-
//
|
|
1264
|
-
|
|
1265
|
-
this.camera.position.set(-22.03, 3.17, 39.12);
|
|
1266
|
-
// Убедимся, что FOV соответствует целевому искаженению (и сохраним кадрирование)
|
|
1267
|
-
this.setPerspectiveFov(20, { keepFraming: true, log: true });
|
|
1415
|
+
// Логирование включается через ?frameDebug=1
|
|
1416
|
+
let log = false;
|
|
1268
1417
|
try {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1418
|
+
const params = new URLSearchParams(window.location.search);
|
|
1419
|
+
log = params.get('frameDebug') === '1';
|
|
1420
|
+
} catch (_) {}
|
|
1421
|
+
|
|
1422
|
+
// Кадрируем строго по центру bbox и с запасом (2×2 из 4×4 => padding≈2.0, +extraPadding)
|
|
1423
|
+
try {
|
|
1424
|
+
this.frameObjectToViewportGrid?.(object3D, {
|
|
1425
|
+
gridCols: 4,
|
|
1426
|
+
gridRows: 4,
|
|
1427
|
+
spanCols: 2,
|
|
1428
|
+
spanRows: 2,
|
|
1429
|
+
extraPadding: 1.05,
|
|
1430
|
+
perspectiveFov: 20,
|
|
1431
|
+
viewDir: new THREE.Vector3(-1, 0.6, 1), // front-right-top
|
|
1432
|
+
log,
|
|
1433
|
+
});
|
|
1434
|
+
} catch (_) {}
|
|
1435
|
+
|
|
1436
|
+
// После выставления камеры — ещё раз подстроим near/far и лимиты зума под новый кадр.
|
|
1437
|
+
try { this.applyAdaptiveZoomLimits(object3D, { padding: 2.1, slack: 2.5, minRatio: 0.05, recenter: false }); } catch (_) {}
|
|
1287
1438
|
|
|
1288
1439
|
// Снимем актуальный «домашний» вид после всех корректировок
|
|
1289
1440
|
this._home.cameraPos = this.camera.position.clone();
|
|
1290
1441
|
this._home.target = this.controls.target.clone();
|
|
1442
|
+
this._home.perspFov = (this.camera && this.camera.isPerspectiveCamera) ? this.camera.fov : (this._projection?.persp?.fov ?? this._home.perspFov ?? 20);
|
|
1291
1443
|
this._home.edgesVisible = this.edgesVisible;
|
|
1292
1444
|
this._home.flatShading = this.flatShading;
|
|
1293
1445
|
this._home.quality = this.quality;
|
|
@@ -3438,6 +3590,88 @@ export class Viewer {
|
|
|
3438
3590
|
this.controls.update();
|
|
3439
3591
|
}
|
|
3440
3592
|
|
|
3593
|
+
/**
|
|
3594
|
+
* Home (view only): возвращает ТОЛЬКО ракурс и масштаб (камера/target/zoom),
|
|
3595
|
+
* не сбрасывая активные инструменты (сечения/рёбра/тени/проекция/качество и т.п.).
|
|
3596
|
+
*/
|
|
3597
|
+
goHomeViewOnly() {
|
|
3598
|
+
if (!this.camera || !this.controls) return;
|
|
3599
|
+
|
|
3600
|
+
// Сброс MMB-pan (viewOffset) — это часть "положения на экране"
|
|
3601
|
+
try { this._mmbPan?.controller?.reset?.(); } catch (_) {}
|
|
3602
|
+
|
|
3603
|
+
const homeTarget = this._home?.target;
|
|
3604
|
+
const homePos = this._home?.cameraPos;
|
|
3605
|
+
if (!homeTarget || !homePos) return;
|
|
3606
|
+
|
|
3607
|
+
// Прицел
|
|
3608
|
+
try { this.controls.target.copy(homeTarget); } catch (_) {}
|
|
3609
|
+
|
|
3610
|
+
// Восстановление масштаба:
|
|
3611
|
+
// - perspective: FOV + position
|
|
3612
|
+
// - ortho: zoom (подбираем так, чтобы видимый halfHeight соответствовал "домашнему" перспективному кадру)
|
|
3613
|
+
const homeFov = (this._home?.perspFov ?? this._projection?.persp?.fov ?? 20);
|
|
3614
|
+
|
|
3615
|
+
if (this.camera.isPerspectiveCamera) {
|
|
3616
|
+
if (Number.isFinite(homeFov) && homeFov > 1e-3 && homeFov < 179) {
|
|
3617
|
+
this.camera.fov = homeFov;
|
|
3618
|
+
}
|
|
3619
|
+
this.camera.position.copy(homePos);
|
|
3620
|
+
this.camera.updateProjectionMatrix();
|
|
3621
|
+
this.controls.update();
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
if (this.camera.isOrthographicCamera) {
|
|
3626
|
+
// Ориентация/направление — как в домашнем виде (через homePos относительно target)
|
|
3627
|
+
const dirVec = homePos.clone().sub(homeTarget);
|
|
3628
|
+
let dist = dirVec.length();
|
|
3629
|
+
if (!(dist > 1e-6)) dist = 1.0;
|
|
3630
|
+
const dirN = dirVec.multiplyScalar(1 / dist);
|
|
3631
|
+
|
|
3632
|
+
// Ставим камеру на ту же дистанцию/направление — для Ortho это влияет на "ракурс", но не на масштаб
|
|
3633
|
+
this.camera.position.copy(homeTarget.clone().add(dirN.clone().multiplyScalar(dist)));
|
|
3634
|
+
|
|
3635
|
+
// Хотим, чтобы видимый halfHeight совпадал с тем, что был бы в перспективе:
|
|
3636
|
+
// halfVisible = dist * tan(fov/2)
|
|
3637
|
+
const vFov = (Number(homeFov) * Math.PI) / 180;
|
|
3638
|
+
const halfVisible = Math.max(0.01, dist * Math.tan(vFov / 2));
|
|
3639
|
+
|
|
3640
|
+
const aspect = this._getAspect?.() || (this.camera.aspect || 1);
|
|
3641
|
+
let halfH = this._projection?.orthoHalfHeight || Math.abs(this.camera.top) || 10;
|
|
3642
|
+
halfH = Math.max(0.01, halfH);
|
|
3643
|
+
|
|
3644
|
+
const minZoom = this.controls?.minZoom ?? this._projection?.minZoom ?? 0.25;
|
|
3645
|
+
const maxZoom = this.controls?.maxZoom ?? this._projection?.maxZoom ?? 8;
|
|
3646
|
+
|
|
3647
|
+
let zoomFit = halfH / Math.max(1e-6, halfVisible);
|
|
3648
|
+
if (zoomFit < minZoom) {
|
|
3649
|
+
// Расширим фрустум так, чтобы при minZoom кадр влезал
|
|
3650
|
+
halfH = Math.max(halfH, halfVisible * minZoom);
|
|
3651
|
+
try { this._projection.orthoHalfHeight = halfH; } catch (_) {}
|
|
3652
|
+
try {
|
|
3653
|
+
this.camera.left = -halfH * aspect;
|
|
3654
|
+
this.camera.right = halfH * aspect;
|
|
3655
|
+
this.camera.top = halfH;
|
|
3656
|
+
this.camera.bottom = -halfH;
|
|
3657
|
+
} catch (_) {}
|
|
3658
|
+
zoomFit = halfH / Math.max(1e-6, halfVisible);
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
const zoom = Math.min(maxZoom, zoomFit);
|
|
3662
|
+
this.camera.zoom = Math.max(1e-6, zoom);
|
|
3663
|
+
|
|
3664
|
+
this.camera.updateProjectionMatrix();
|
|
3665
|
+
this.controls.update();
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// На неизвестной камере просто восстановим позицию
|
|
3670
|
+
try { this.camera.position.copy(homePos); } catch (_) {}
|
|
3671
|
+
try { this.camera.updateProjectionMatrix(); } catch (_) {}
|
|
3672
|
+
try { this.controls.update(); } catch (_) {}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3441
3675
|
// ================= Вспомогательное: ось вращения =================
|
|
3442
3676
|
#ensureRotationAxisLine() {
|
|
3443
3677
|
if (this.rotationAxisLine) return this.rotationAxisLine;
|