player/src/engine/GizmoController.js
МИН 87444ee2c8 Initial public release: Rublox Player v1.0
Open-source web player for Rublox games, dual-licensed under
AGPL-3.0 + Commercial.

Highlights:
- Babylon.js 7 + React 18 + Vite 5 stack
- Self-contained engine (~46k lines): BlockManager, ModelManager,
  PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD
  gamemodes
- Configurable backend via VITE_API_BASE and friends — works against
  staging (dev-api.rublox.pro) out of the box
- Standalone mode (VITE_STANDALONE=true) loads a bundled sample game
  for first-run without any backend
- Full docs: README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG
- Lint + format scaffolding (ESLint + Prettier + EditorConfig)
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Removed before public release:
- frontend_deploy.py (contained production SSH credentials)
- ~27 admin endpoints (kept in private repo)
- Hard-coded internal URLs and IPs
- All previous git history (clean repo init)
2026-05-27 23:04:04 +03:00

183 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* GizmoController — обёртка над Babylon GizmoManager.
*
* Поддерживает 3 режима манипуляции:
* 'move' — стрелки X/Y/Z для перемещения (PositionGizmo)
* 'rotate' — кольца X/Y/Z для поворота (RotationGizmo)
* 'scale' — кубики X/Y/Z для масштабирования (ScaleGizmo)
*
* Также режим 'select' = ничего не показывать (просто выделение без гизмо).
*
* Snap-режим (для move): 1.0 / 0.5 / 0.25 / 0 (off).
*
* При завершении drag вызывает onDragEnd колбэк.
*
* ВАЖНО: GizmoManager создаёт sub-gizmos (positionGizmo и т.п.) лениво
* при первом включении соответствующего режима. Поэтому slушатели и snap
* нужно ставить ПОСЛЕ включения режима, не до.
*/
import { GizmoManager } from '@babylonjs/core';
export class GizmoController {
constructor(layer, scene) {
this.scene = scene;
this.layer = layer;
// 1.5 — общий размер гизмо. usePointerToAttach=false чтобы клик по объекту
// не назначал гизмо автоматически — это делает наш SelectionManager.
this.manager = new GizmoManager(scene, 1.5, layer);
this.manager.usePointerToAttachGizmos = false;
this._mode = 'select';
this._snap = 1.0;
this._attached = null; // mesh или TransformNode на который указывает гизмо
this._onDragEnd = null;
this._onDragStart = null;
// Set гизмо у которых уже привязаны слушатели (чтобы не делать повторно)
this._listenersBound = new Set();
}
setOnDragEnd(cb) { this._onDragEnd = cb; }
setOnDragStart(cb) { this._onDragStart = cb; }
/** Главный метод — переключить режим. */
setMode(mode) {
this._mode = mode;
this.manager.positionGizmoEnabled = false;
this.manager.rotationGizmoEnabled = false;
this.manager.scaleGizmoEnabled = false;
if (mode === 'move') this.manager.positionGizmoEnabled = true;
if (mode === 'rotate') this.manager.rotationGizmoEnabled = true;
if (mode === 'scale') this.manager.scaleGizmoEnabled = true;
this._bindListenersFor(mode);
this._applySnap();
this._reattach();
}
/**
* Переустановить активный режим — пересоздаёт sub-gizmo с привязкой
* к текущему _attached. Нужен после смены выделения, чтобы стрелки
* гарантированно отвечали на драг.
*/
refreshMode() {
const m = this._mode;
if (m === 'select') return;
// Сначала выключаем все sub-gizmo, потом включаем нужный заново —
// Babylon при включении sub-gizmo возьмёт актуальный attachedMesh.
this.manager.positionGizmoEnabled = false;
this.manager.rotationGizmoEnabled = false;
this.manager.scaleGizmoEnabled = false;
if (m === 'move') this.manager.positionGizmoEnabled = true;
if (m === 'rotate') this.manager.rotationGizmoEnabled = true;
if (m === 'scale') this.manager.scaleGizmoEnabled = true;
this._bindListenersFor(m);
this._applySnap();
// _reattach уже сделан в setMode, но повторим — на случай что
// attached изменился между вызовами.
this._reattach();
}
getMode() { return this._mode; }
/** Привязать drag-листенеры к gizmo текущего режима если ещё не привязаны. */
_bindListenersFor(mode) {
let gizmo = null;
if (mode === 'move') gizmo = this.manager.gizmos.positionGizmo;
if (mode === 'rotate') gizmo = this.manager.gizmos.rotationGizmo;
if (mode === 'scale') gizmo = this.manager.gizmos.scaleGizmo;
if (!gizmo || this._listenersBound.has(gizmo)) return;
gizmo.onDragStartObservable.add(() => {
if (this._onDragStart) this._onDragStart();
});
gizmo.onDragEndObservable.add(() => {
if (this._onDragEnd) this._onDragEnd();
});
this._listenersBound.add(gizmo);
}
/**
* Прикрепить гизмо к mesh-объекту (или null чтобы отключить).
* Должен вызываться после setMode (или setMode сам делает _reattach).
*/
attachTo(mesh) {
this._attached = mesh;
this._reattach();
}
_reattach() {
const target = this._attached;
// Безопасно отвязываем гизмо
const detach = () => {
try { this.manager.attachToMesh(null); } catch (e) { /* ignore */ }
if (typeof this.manager.attachToNode === 'function') {
try { this.manager.attachToNode(null); } catch (e) { /* ignore */ }
}
};
if (!target) { detach(); return; }
// Proxy-меши блоков (thin-instances) и liquid-proxy не имеют API Mesh
// (нет removeBehavior, getWorldMatrix). Гизмо к ним не подвязывается —
// блоки и так стоят на сетке, перемещать их через гизмо некуда.
const isProxy = !!(target._isBlockProxy || target.metadata?._liquidProxy);
if (isProxy) { detach(); return; }
// Проверяем что у узла есть нужный API (Mesh / TransformNode полноценный).
const hasNodeApi = typeof target.removeBehavior === 'function'
&& typeof target.getWorldMatrix === 'function';
if (!hasNodeApi) { detach(); return; }
const isMesh = typeof target.getTotalVertices === 'function'
&& target.getTotalVertices() > 0;
try {
if (isMesh) {
this.manager.attachToMesh(target);
} else if (typeof this.manager.attachToNode === 'function') {
this.manager.attachToNode(target);
} else {
detach();
}
} catch (e) {
// eslint-disable-next-line no-console
console.warn('[GizmoController] attach failed', e);
detach();
}
}
/** Установить snap step. */
setSnap(step) {
this._snap = step;
this._applySnap();
}
/** Применить snap к текущим sub-gizmo (если они созданы). */
_applySnap() {
const step = this._snap;
const pg = this.manager.gizmos.positionGizmo;
if (pg) {
pg.snapDistance = step > 0 ? step : 0;
// У PositionGizmo есть отдельные xGizmo/yGizmo/zGizmo,
// и planarGizmoEnabled (плоскостные). Babylon 5+ автоматически
// прокидывает snapDistance на оси. На всякий случай — явно:
if (pg.xGizmo) pg.xGizmo.snapDistance = pg.snapDistance;
if (pg.yGizmo) pg.yGizmo.snapDistance = pg.snapDistance;
if (pg.zGizmo) pg.zGizmo.snapDistance = pg.snapDistance;
}
const rg = this.manager.gizmos.rotationGizmo;
if (rg) {
// Для rotation snap — в радианах. 15° = π/12.
const rad = step > 0 ? Math.PI / 12 : 0;
rg.snapDistance = rad;
if (rg.xGizmo) rg.xGizmo.snapDistance = rad;
if (rg.yGizmo) rg.yGizmo.snapDistance = rad;
if (rg.zGizmo) rg.zGizmo.snapDistance = rad;
}
}
getSnap() { return this._snap; }
dispose() {
try { this.manager.dispose(); } catch (e) { /* ignore */ }
}
}