fix(studio): групповые манипуляции папки в реальном времени (не телепорт)

Дельта пивота применялась только в dragEnd → объекты телепортировались в
конце. Теперь _onFolderGizmoDrag применяет инкрементальную дельту на каждом
тике (setOnDrag) — движение/вращение/масштаб группы видно в процессе, как
у одиночных объектов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-05 02:53:35 +03:00
parent 8b887e866a
commit c7b5f3645d

View File

@ -1367,7 +1367,12 @@ export class BabylonScene {
this._gizmo.setOnDragEnd(() => this._onGizmoDragEnd()); this._gizmo.setOnDragEnd(() => this._onGizmoDragEnd());
// Во время scale-drag — live-обновление тайлинга studs (кружки одного // Во время scale-drag — live-обновление тайлинга studs (кружки одного
// размера, не растягиваются пока тянешь гизмо). // размера, не растягиваются пока тянешь гизмо).
this._gizmo.setOnDrag((mode) => { if (mode === 'scale') this._onGizmoScaleDrag(); }); this._gizmo.setOnDrag((mode) => {
if (mode === 'scale') this._onGizmoScaleDrag();
// Групповая папка — применяем дельту в реальном времени (видно движение).
const sel = this.selection?.getSelection?.();
if (sel && sel.type === 'folder') this._onFolderGizmoDrag(mode);
});
// Привязка гизмо к выделенному // Привязка гизмо к выделенному
this.selection.setOnSelectionChange((sel) => this._updateGizmoForSelection(sel)); this.selection.setOnSelectionChange((sel) => this._updateGizmoForSelection(sel));
@ -3756,36 +3761,59 @@ export class BabylonScene {
pivot.scaling = new Vector3(1, 1, 1); pivot.scaling = new Vector3(1, 1, 1);
this._folderPivot = pivot; this._folderPivot = pivot;
this._folderPivotId = folderId; this._folderPivotId = folderId;
// Запоминаем стартовое состояние пивота для вычисления дельты в dragEnd. // «Последнее применённое» состояние пивота — для инкрементальной
this._folderPivotStart = { // дельты в реальном времени (_onFolderGizmoDrag).
pos: { x: center.x, y: center.y, z: center.z }, this._folderPivotLast = {
x: center.x, y: center.y, z: center.z,
ry: 0, scale: 1,
center: { x: center.x, y: center.y, z: center.z }, center: { x: center.x, y: center.y, z: center.z },
}; };
} catch (e) { console.warn('[folderGizmo] attach failed', e); } } catch (e) { console.warn('[folderGizmo] attach failed', e); }
} }
/** Применить трансформацию пивота к объектам папки (вызывается на dragEnd). */ /**
_applyFolderGizmo(mode) { * Инкрементально применить движение/поворот/масштаб пивота к объектам папки
* ПРЯМО ВО ВРЕМЯ drag (чтобы было видно перемещение, а не телепорт в конце).
*/
_onFolderGizmoDrag(mode) {
const pivot = this._folderPivot; const pivot = this._folderPivot;
const fid = this._folderPivotId; const fid = this._folderPivotId;
const start = this._folderPivotStart; const last = this._folderPivotLast;
if (!pivot || fid == null || !start || !this.folderManager) return; if (!pivot || fid == null || !last || !this.folderManager) return;
if (mode === 'move') { if (mode === 'move') {
const dx = pivot.position.x - start.pos.x; const dx = pivot.position.x - last.x;
const dy = pivot.position.y - start.pos.y; const dy = pivot.position.y - last.y;
const dz = pivot.position.z - start.pos.z; const dz = pivot.position.z - last.z;
if (dx || dy || dz) this.folderManager.moveFolderBy(fid, dx, dy, dz); if (dx || dy || dz) {
} else if (mode === 'rotate') { this.folderManager.moveFolderBy(fid, dx, dy, dz);
const ry = pivot.rotation.y; last.x = pivot.position.x; last.y = pivot.position.y; last.z = pivot.position.z;
if (Math.abs(ry) > 0.0001) this.folderManager.rotateFolderY(fid, ry, start.center); last.center.x += dx; last.center.y += dy; last.center.z += dz;
} else if (mode === 'scale') {
const f = (pivot.scaling.x + pivot.scaling.y + pivot.scaling.z) / 3;
if (Math.abs(f - 1) > 0.001) this.folderManager.scaleFolder(fid, f, start.center);
} }
// Пересоздаём пивот в новом центре (сброс дельты для следующего drag). } else if (mode === 'rotate') {
const dRy = pivot.rotation.y - last.ry;
if (Math.abs(dRy) > 0.0001) {
this.folderManager.rotateFolderY(fid, dRy, last.center);
last.ry = pivot.rotation.y;
}
} else if (mode === 'scale') {
const cur = (pivot.scaling.x + pivot.scaling.y + pivot.scaling.z) / 3;
const factor = last.scale !== 0 ? cur / last.scale : 1;
if (Math.abs(factor - 1) > 0.001) {
this.folderManager.scaleFolder(fid, factor, last.center);
last.scale = cur;
}
}
}
/** dragEnd: дельта уже применена в _onFolderGizmoDrag — пересоздаём пивот. */
_applyFolderGizmo(mode) {
const fid = this._folderPivotId;
if (fid == null || !this.folderManager) return;
// На всякий случай добираем остаток дельты (если drag был очень коротким).
this._onFolderGizmoDrag(mode);
// Пересоздаём пивот в новом центре (сброс для следующего drag).
const g = this.folderManager.getFolderObjects(fid); const g = this.folderManager.getFolderObjects(fid);
this._attachFolderGizmo(fid, g.center); this._attachFolderGizmo(fid, g.center);
// Переустановить gizmo на новый пивот + обновить selection.center.
const sel = this.selection?.getSelection?.(); const sel = this.selection?.getSelection?.();
if (sel && sel.type === 'folder') { sel.center = g.center; } if (sel && sel.type === 'folder') { sel.center = g.center; }
if (this._gizmo && this._folderPivot) { if (this._gizmo && this._folderPivot) {