/** * CollabOverlay — DOM-слой совместного редактирования (Team Create). * * Рисует поверх canvas студии: * - панель «онлайн» (аватарки-кружки соавторов с цветом и ником); * - 3D-курсоры соавторов (точка с ником, спроецированная из мировых * координат в экранные); * - подсветку чужого выделения (по selectedKey — обводим объект); * - тосты («объект занят другим», «N присоединился»). * * Самодостаточный (как LoadingScreenOverlay): монтируется на parent canvas, * UI обновляется методами update(...). Кнопка «Пригласить» — снаружи (TopRibbon), * здесь только presence + курсоры. */ import { Vector3, Matrix } from '@babylonjs/core'; let _cssInjected = false; function injectCss() { if (_cssInjected || typeof document === 'undefined') return; _cssInjected = true; const s = document.createElement('style'); s.id = 'kbn-collab-css'; s.textContent = '.kbnCollabCursor{position:absolute;transform:translate(-50%,-50%);pointer-events:none;z-index:62;transition:left 0.08s linear,top 0.08s linear;will-change:left,top}' + '.kbnCollabDot{width:14px;height:14px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.5)}' + '.kbnCollabName{position:absolute;left:16px;top:-2px;white-space:nowrap;font:600 11px system-ui;color:#fff;padding:1px 6px;border-radius:6px;text-shadow:0 1px 2px rgba(0,0,0,.6)}' + '.kbnCollabBar{position:absolute;top:10px;left:50%;transform:translateX(-50%);z-index:63;display:flex;gap:6px;align-items:center;background:rgba(20,24,38,.78);padding:5px 10px;border-radius:20px;backdrop-filter:blur(6px);font:600 12px system-ui;color:#cdd6e6;pointer-events:auto}' + '.kbnCollabAva{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font:700 12px system-ui;color:#10131c;border:2px solid rgba(255,255,255,.25)}' + '.kbnCollabToast{position:absolute;bottom:80px;left:50%;transform:translateX(-50%);z-index:64;background:rgba(20,24,38,.92);color:#fff;padding:10px 18px;border-radius:10px;font:600 14px system-ui;box-shadow:0 8px 24px rgba(0,0,0,.5);opacity:0;transition:opacity .25s}'; document.head.appendChild(s); } export class CollabOverlay { constructor(scene) { this.scene = scene; this.root = null; this.barEl = null; this.cursorsEl = null; this.toastEl = null; this._cursors = new Map(); // sessionId → {wrap, dot, name} this._presence = []; this._raf = null; } mount() { injectCss(); const parent = (this.scene.canvas && this.scene.canvas.parentElement) || document.body; try { if (getComputedStyle(parent).position === 'static') parent.style.position = 'relative'; } catch (e) { /* ignore */ } const root = document.createElement('div'); root.className = 'kbn-collab-root'; root.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:60;overflow:hidden'; const bar = document.createElement('div'); bar.className = 'kbnCollabBar'; bar.innerHTML = '👥 Соавторы:'; const cursors = document.createElement('div'); cursors.style.cssText = 'position:absolute;inset:0'; const toast = document.createElement('div'); toast.className = 'kbnCollabToast'; root.appendChild(bar); root.appendChild(cursors); root.appendChild(toast); parent.appendChild(root); this.root = root; this.barEl = bar; this.cursorsEl = cursors; this.toastEl = toast; this._startLoop(); } /** Обновить список соавторов (из StudioCollab.onPresenceChange). */ updatePresence(list) { this._presence = list || []; if (!this.barEl) return; // Перерисовать аватарки. const others = this._presence; let html = '👥'; for (const c of others) { const initial = (c.username || '?').trim().charAt(0).toUpperCase() || '?'; const ring = c.me ? 'box-shadow:0 0 0 2px #fff' : ''; html += `