@sequent-org/ifc-viewer 1.2.4-ci.45.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.
@@ -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 со смещением вниз по Y (как при первичной загрузке).
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
- const size = box.getSize(new THREE.Vector3());
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 + verticalBias)
272
+ * - иначе используем "домашний" pivot (центр bbox)
276
273
  * @returns {THREE.Vector3|null}
277
274
  */
278
275
  #getDesiredPivotForRotate() {
@@ -455,14 +452,13 @@ export class Viewer {
455
452
  // logarithmicDepthBuffer: уменьшает z-fighting на почти копланарных поверхностях (часто в IFC).
456
453
  // Это заметно снижает "мигание" тонких накладных деталей на фасадах.
457
454
  // stencil: нужен для отрисовки "cap" по контуру сечения
458
- // ВАЖНО: фон всегда должен быть чисто белым и не зависеть от CSS-окружения (модалки/страницы).
459
- // Поэтому делаем рендер НЕпрозрачным (alpha: false) и задаём белый clearColor.
460
- this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, logarithmicDepthBuffer: true, stencil: true });
455
+ // Фон должен выглядеть белым всегда, но при этом сохраняем прежнее поведение рендера (прозрачный canvas),
456
+ // чтобы не менять визуальное смешивание (тени/пост-эффекты) относительно версии "до белого фона".
457
+ // Белизна обеспечивается фоном контейнера (IfcViewer.js), а canvas остаётся прозрачным.
458
+ this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true, stencil: true });
461
459
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
462
460
  this.renderer.autoClear = false; // управляем очисткой вручную для мульти-проходов
463
- try {
464
- this.renderer.setClearColor(0xffffff, 1);
465
- } catch (_) {}
461
+ try { this.renderer.setClearColor(0xffffff, 0); } catch (_) {}
466
462
  // Тени по умолчанию выключены (включаются только через setShadowsEnabled)
467
463
  this.renderer.shadowMap.enabled = false;
468
464
  this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
@@ -485,8 +481,8 @@ export class Viewer {
485
481
 
486
482
  // Сцена
487
483
  this.scene = new THREE.Scene();
488
- // Гарантируем белый фон сцены (в том числе для composer/pass-ов).
489
- try { this.scene.background = new THREE.Color(0xffffff); } catch (_) {}
484
+ // Оставляем фон сцены прозрачным (белый фон задаётся контейнером).
485
+ try { this.scene.background = null; } catch (_) {}
490
486
  // Оверлей-сцена для секущих манипуляторов (без клиппинга)
491
487
  this.sectionOverlayScene = new THREE.Scene();
492
488
 
@@ -666,7 +662,8 @@ export class Viewer {
666
662
  sizePx: 96,
667
663
  marginPx: 10,
668
664
  opacity: 0.6,
669
- onHome: () => this.goHome(),
665
+ // Home-кнопка NavCube: возвращаем ТОЛЬКО камеру (не трогаем инструменты/сечения/стили)
666
+ onHome: () => this.goHomeViewOnly(),
670
667
  });
671
668
 
672
669
  // Визуальная ось вращения: события мыши и контролов
@@ -741,6 +738,7 @@ export class Viewer {
741
738
  // Сохраним Home-снапшот после инициализации
742
739
  this._home.cameraPos = this.camera.position.clone();
743
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);
744
742
  this._home.edgesVisible = this.edgesVisible;
745
743
  this._home.flatShading = this.flatShading;
746
744
  this._home.quality = this.quality;
@@ -1033,6 +1031,160 @@ export class Viewer {
1033
1031
  }
1034
1032
  }
1035
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
+
1036
1188
  // Вычисляет дистанцию до объекта, при которой он полностью помещается в кадр
1037
1189
  #computeFitDistanceForSize(size, padding = 1.2) {
1038
1190
  // Защита от нулевых размеров
@@ -1247,48 +1399,47 @@ export class Viewer {
1247
1399
  // Синхронизируем "сечение → shadow-pass" для материалов после назначения пресета
1248
1400
  this.#applyClipShadowsToModelMaterials();
1249
1401
 
1250
- // Настроим пределы зума и сфокусируемся на новой модели
1251
- this.applyAdaptiveZoomLimits(object3D, { padding: 1.2, slack: 2.5, minRatio: 0.05, recenter: true });
1402
+ // Настроим пределы зума под габариты модели (кадрирование делаем отдельно ниже, на следующем кадре).
1403
+ // Здесь важно в первую очередь "оздоровить" near/far под размер модели.
1404
+ this.applyAdaptiveZoomLimits(object3D, { padding: 2.1, slack: 2.5, minRatio: 0.05, recenter: false });
1252
1405
 
1253
1406
  // Если "Тест" активен, сразу применим его к только что загруженной модели (самозатенение + shadow camera по bbox)
1254
1407
  if (this._testPreset?.enabled) {
1255
1408
  try { this.#applyTestPresetToScene(); } catch (_) {}
1256
1409
  }
1257
1410
 
1258
- // На следующем кадре отъедем на 2x от вписанной дистанции (точно по размеру модели)
1411
+ // На следующем кадре выставим кадрирование безопасной зоне" (2×2 из 4×4) и ракурс front-right-top.
1259
1412
  try {
1260
- const box = new THREE.Box3().setFromObject(object3D);
1261
- const center = box.getCenter(new THREE.Vector3());
1262
1413
  requestAnimationFrame(() => {
1263
1414
  if (!this.camera || !this.controls) return;
1264
- // Центрируем точку взгляда на центр модели и ставим камеру в заданные координаты
1265
- this.controls.target.copy(center);
1266
- this.camera.position.set(-22.03, 3.17, 39.12);
1267
- // Убедимся, что FOV соответствует целевому искаженению (и сохраним кадрирование)
1268
- this.setPerspectiveFov(20, { keepFraming: true, log: true });
1415
+ // Логирование включается через ?frameDebug=1
1416
+ let log = false;
1269
1417
  try {
1270
- // Если камера слишком близко, отъедем до вписанной дистанции, сохранив направление
1271
- const size = box.getSize(new THREE.Vector3());
1272
- const fitDistExact = this.#computeFitDistanceForSize(size, 1.2);
1273
- const dirVec = this.camera.position.clone().sub(center);
1274
- const dist = dirVec.length();
1275
- if (dist < fitDistExact && dist > 1e-6) {
1276
- const dirNorm = dirVec.multiplyScalar(1 / dist);
1277
- this.camera.position.copy(center.clone().add(dirNorm.multiplyScalar(fitDistExact)));
1278
- }
1279
- // Поднимем модель в кадре: сместим точку прицеливания немного вниз по Y
1280
- const verticalBias = size.y * 0.30; // 30% высоты
1281
- this.controls.target.y = center.y - verticalBias;
1282
- } catch(_) {}
1283
- // После ручной перестановки камеры ещё раз "оздоровим" near/far под модель,
1284
- // чтобы не ловить z-fighting на фасадных накладках.
1285
- try { this.applyAdaptiveZoomLimits(object3D, { padding: 1.2, slack: 2.5, minRatio: 0.05, recenter: false }); } catch (_) {}
1286
- this.camera.updateProjectionMatrix();
1287
- this.controls.update();
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 (_) {}
1288
1438
 
1289
1439
  // Снимем актуальный «домашний» вид после всех корректировок
1290
1440
  this._home.cameraPos = this.camera.position.clone();
1291
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);
1292
1443
  this._home.edgesVisible = this.edgesVisible;
1293
1444
  this._home.flatShading = this.flatShading;
1294
1445
  this._home.quality = this.quality;
@@ -2674,12 +2825,12 @@ export class Viewer {
2674
2825
  * @param {boolean} enabled
2675
2826
  */
2676
2827
  setStep3BackgroundEnabled(enabled) {
2677
- // По требованиям: фон ВСЕГДА чисто белый, и шаг 3 не должен менять фон сцены.
2678
- // Поэтому игнорируем "включение" и принудительно удерживаем белый фон.
2828
+ // По требованиям: фон ВСЕГДА белый (за счёт контейнера), и шаг 3 не должен менять фон сцены.
2829
+ // Поэтому игнорируем "включение" и удерживаем background прозрачным.
2679
2830
  if (!this.scene) return;
2680
2831
  this._step3Background.enabled = false;
2681
2832
  this._step3Background.snapshot = null;
2682
- try { this.scene.background = new THREE.Color(0xffffff); } catch (_) {}
2833
+ try { this.scene.background = null; } catch (_) {}
2683
2834
  }
2684
2835
 
2685
2836
  setAOEnabled(enabled) {
@@ -3439,6 +3590,88 @@ export class Viewer {
3439
3590
  this.controls.update();
3440
3591
  }
3441
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
+
3442
3675
  // ================= Вспомогательное: ось вращения =================
3443
3676
  #ensureRotationAxisLine() {
3444
3677
  if (this.rotationAxisLine) return this.rotationAxisLine;