(function (global) { 'use strict'; var STORAGE_KEY = 'preview-workbench-layout-v7'; var PANEL_MEM_KEY = 'preview-workbench-panel-mem-v1'; var COMPACT_W = 960; var COMPACT_SHORT = 720; var PANEL_TITLES = { hierarchy: '层级', inspector: '属性', assets: '资源', perf: '性能', console: '控制台', }; var SPLIT_SIZE = 4; /** * Creator 风格递归停靠布局: * - 拖到标题栏/中心区 = 合并为同一区域页签 * - 拖到区域边沿 = 在该方向新建布局区域 * @constructor */ function WorkbenchLayout(root) { this.root = root; this.workspace = document.getElementById('pw-workspace'); this.gameHost = document.getElementById('pw-game-host'); this.dropOverlay = document.getElementById('pw-drop-overlay'); this.mode = 'desktop'; this.preset = 'analyze'; this.activeDrawer = ''; this._panelBodies = {}; this._panelMeta = {}; this._tree = null; this._leafEls = {}; this._nodeEls = {}; this._dragPanel = null; this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; this._splitDrag = null; this._leafSeq = 1; /** 用户是否已改过布局(拖拽/开关面板);未改动时引擎就绪可按横竖屏重套默认 */ this._dirty = false; /** @type {Object.} 面板开关记忆 */ this._panelMemory = {}; /** @type {Object.} */ this._floats = {}; this._floatLayer = null; this._floatDrag = null; /** @type {function(Object.):void|null} */ this.onVisibilityChange = null; /** @type {HTMLElement|null} 游戏占位格(iframe 永不插入此处) */ this._gamePlaceholder = null; /** 关闭工作台时隐藏调试面板,仍保留设备预览选项(默认开,由 Host 按偏好关闭) */ this._workbenchOff = false; this._treeBeforeOff = null; this._onResize = this._onWindowResize.bind(this); this._onDragOver = this._onDocDragOver.bind(this); this._onDrop = this._onDocDrop.bind(this); this._onDragEnd = this._onDocDragEnd.bind(this); this._onFloatMove = this._onFloatMove.bind(this); this._onFloatUp = this._onFloatUp.bind(this); } WorkbenchLayout.prototype.init = function () { this._load(); this._ensureFloatLayer(); if (!this._tree) this._tree = this._presetTree(this.preset); this._bindToolbar(); window.addEventListener('resize', this._onResize); document.addEventListener('dragover', this._onDragOver); document.addEventListener('drop', this._onDrop); document.addEventListener('dragend', this._onDragEnd); this.refreshMode(); this.rebuild(); this._restoreFloats(); }; WorkbenchLayout.prototype._notifyViewResize = function () { if (global.__PreviewWorkbench && global.__PreviewWorkbench.device) { global.__PreviewWorkbench.device.apply(false); } else if (global.__PW_Bridge) { global.__PW_Bridge.call('device.apply', { force: true }).catch(function () { /* */ }); } }; /** @returns {Object.} */ WorkbenchLayout.prototype.getVisiblePanels = function () { var map = {}; var ids = ['hierarchy', 'inspector', 'assets', 'perf', 'console']; for (var i = 0; i < ids.length; i++) { map[ids[i]] = this.isPanelVisible(ids[i]); } return map; }; /** @param {string} panelId */ WorkbenchLayout.prototype.isPanelVisible = function (panelId) { if (this._workbenchOff) return false; if (this.mode === 'compact') return this.activeDrawer === panelId; if (this._floats[panelId]) { var f = this._floats[panelId]; if (f && f.el && f.el.hidden) return false; return true; } return !!this._findTabsWith(this._tree, panelId); }; WorkbenchLayout.prototype._emitVisibility = function () { if (typeof this.onVisibilityChange === 'function') { this.onVisibilityChange(this.getVisiblePanels()); } }; WorkbenchLayout.prototype.registerPanel = function (id, el, title) { this._panelBodies[id] = el; this._panelMeta[id] = { title: title || PANEL_TITLES[id] || id }; if (!el.classList.contains('pw-panel-root')) { el.classList.add('pw-panel-root'); } // 去掉面板自带标题(区域页签负责标题) var head = null; for (var i = 0; i < el.children.length; i++) { if (el.children[i].classList.contains('pw-panel-head')) { head = el.children[i]; break; } } if (head) { // 保留工具条到面板顶部工具行 var tools = head.querySelector('.pw-panel-tools'); if (tools && tools.childNodes.length) { var bar = document.createElement('div'); bar.className = 'pw-panel-toolbar'; while (tools.firstChild) bar.appendChild(tools.firstChild); head.parentNode.insertBefore(bar, head); } head.parentNode.removeChild(head); } }; WorkbenchLayout.prototype.refreshMode = function () { var w = window.innerWidth; var h = window.innerHeight; var short = Math.min(w, h); var compact = w < COMPACT_W || short < COMPACT_SHORT; this.mode = compact ? 'compact' : 'desktop'; this.root.classList.toggle('pw-mode-compact', compact); this.root.classList.toggle('pw-mode-desktop', !compact); this.root.classList.toggle('pw-compact-portrait', compact && h >= w); this.root.classList.toggle('pw-compact-landscape', compact && h < w); if (!compact) this.closeDrawer(); }; WorkbenchLayout.prototype.applyPreset = function (name, persist) { this.preset = name || 'analyze'; this._dirty = false; this._tree = this._presetTree(this.preset); var sel = document.getElementById('pw-layout-preset'); if (sel) sel.value = this.preset; this.rebuild(); if (persist !== false) this._save(); }; WorkbenchLayout.prototype.resetLayout = function () { try { localStorage.removeItem(STORAGE_KEY); } catch (e) { /* */ } this._dirty = false; this.applyPreset(this.preset || 'analyze', true); }; /** * 引擎就绪后:未手动改布局时,按设计分辨率横竖屏重套「分析」默认。 */ WorkbenchLayout.prototype.onEngineReady = function () { if (this._dirty) return; if (this.preset !== 'analyze' && this.preset !== 'portrait' && this.preset !== 'landscape') return; if (this.preset === 'analyze') { var next = this._presetTree('analyze'); // 工作台关闭时只更新备份,避免与「仅游戏」树冲突导致布局抖动 if (this._workbenchOff) { this._treeBeforeOff = next; return; } this._tree = next; this.rebuild(); this._save(); } }; /** 设计分辨率是否竖屏(高 ≥ 宽) */ WorkbenchLayout.prototype._isPortraitGame = function () { var cc = global.cc; try { if (cc && cc.view && typeof cc.view.getDesignResolutionSize === 'function') { var dr = cc.view.getDesignResolutionSize(); if (dr && dr.width > 0 && dr.height > 0) return dr.height >= dr.width; } } catch (e) { /* */ } // 本项目默认 750×1600 return true; }; WorkbenchLayout.prototype._presetTree = function (name) { if (name === 'scene') { return { type: 'split', dir: 'h', sizes: [0.22, 0.56, 0.22], children: [ this._tabsNode(['hierarchy']), this._gameNode(), this._tabsNode(['inspector']), ], }; } if (name === 'minimal') { return { type: 'split', dir: 'v', sizes: [0.78, 0.22], children: [ this._gameNode(), this._tabsNode(['perf']), ], }; } if (name === 'portrait') return this._portraitAnalyzeTree(); if (name === 'landscape') return this._landscapeAnalyzeTree(); // analyze:按游戏设计分辨率自动选竖屏/横屏默认 return this._isPortraitGame() ? this._portraitAnalyzeTree() : this._landscapeAnalyzeTree(); }; /** * 竖屏默认(图1): * 左上 = 层级 | 属性 * 左下 = 控制台(横跨层级+属性两列下方) * 中 = 游戏,右 = 性能 */ WorkbenchLayout.prototype._portraitAnalyzeTree = function () { return { type: 'split', dir: 'h', sizes: [0.34, 0.46, 0.20], children: [ { type: 'split', dir: 'v', sizes: [0.58, 0.42], children: [ { type: 'split', dir: 'h', sizes: [0.5, 0.5], children: [ this._tabsNode(['hierarchy']), this._tabsNode(['inspector']), ], }, this._tabsNode(['console']), ], }, this._gameNode(), this._tabsNode(['perf']), ], }; }; /** * 横屏默认(图2):上=层级|属性|游戏|性能,下=控制台通栏 */ WorkbenchLayout.prototype._landscapeAnalyzeTree = function () { return { type: 'split', dir: 'v', sizes: [0.72, 0.28], children: [ { type: 'split', dir: 'h', sizes: [0.14, 0.16, 0.50, 0.20], children: [ this._tabsNode(['hierarchy']), this._tabsNode(['inspector']), this._gameNode(), this._tabsNode(['perf']), ], }, this._tabsNode(['console']), ], }; }; WorkbenchLayout.prototype._tabsNode = function (panels) { return { type: 'tabs', id: 'leaf_' + (this._leafSeq++), panels: panels.slice(), active: panels[0] || '', }; }; WorkbenchLayout.prototype._gameNode = function () { return { type: 'game', id: 'leaf_game' }; }; WorkbenchLayout.prototype._ensureId = function (node) { if (!node) return; if (node.type === 'game') { node.id = 'leaf_game'; return; } if (!node.id) node.id = (node.type === 'split' ? 'split_' : 'leaf_') + (this._leafSeq++); }; WorkbenchLayout.prototype.rebuild = function () { if (!this.workspace) return; // 只卸面板 DOM;游戏 iframe 宿主永不搬移(搬移会导致 iframe 整页重载) for (var id in this._panelBodies) { if (!Object.prototype.hasOwnProperty.call(this._panelBodies, id)) continue; if (this._floats[id]) continue; var p = this._panelBodies[id]; if (p && p.parentNode) p.parentNode.removeChild(p); } this._ensureGameHostParked(); this._gamePlaceholder = null; this._leafEls = {}; this._nodeEls = {}; while (this.workspace.firstChild) this.workspace.removeChild(this.workspace.firstChild); if (this.mode === 'compact') { if (this.gameHost) this.gameHost.classList.add('pw-game-compact'); this._syncGameHostGeom(); this._notifyViewResize(); this._emitVisibility(); return; } if (this.gameHost) this.gameHost.classList.remove('pw-game-compact'); var el = this._renderNode(this._tree); if (el) this.workspace.appendChild(el); var floatIds = []; for (var fid in this._floats) { if (Object.prototype.hasOwnProperty.call(this._floats, fid)) floatIds.push(fid); } for (var fi = 0; fi < floatIds.length; fi++) { var pid = floatIds[fi]; if (this._findTabsWith(this._tree, pid)) { this._closeFloat(pid, false); continue; } var f = this._floats[pid]; var body = this._panelBodies[pid]; var fb = f && f.el ? f.el.querySelector('.pw-float-body') : null; if (fb && body && body.parentNode !== fb) fb.appendChild(body); } var self = this; this._syncGameHostGeom(); if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(function () { self._syncGameHostGeom(); }); } this._notifyViewResize(); this._emitVisibility(); }; /** 游戏宿主固定挂在 root 下,禁止插入停靠树(避免 iframe reload) */ WorkbenchLayout.prototype._ensureGameHostParked = function () { if (!this.gameHost || !this.root) return; if (this.gameHost.parentNode !== this.root) { this.root.appendChild(this.gameHost); } this.gameHost.classList.add('pw-game-overlay'); }; /** 将 overlay 对齐到占位格(或紧凑/关闭模式下的工作区) */ WorkbenchLayout.prototype._syncGameHostGeom = function () { if (!this.gameHost || !this.root) return; this._ensureGameHostParked(); var rootRect = this.root.getBoundingClientRect(); var target = null; if (this._workbenchOff) { target = this.workspace || this.root; } else if (this.mode === 'compact') { target = this.workspace; } else { target = this._gamePlaceholder; } if (!target) { this.gameHost.hidden = true; return; } var r = target.getBoundingClientRect(); if (r.width < 2 || r.height < 2) { this.gameHost.hidden = true; return; } this.gameHost.hidden = false; this.gameHost.style.pointerEvents = 'auto'; this.gameHost.style.left = Math.max(0, r.left - rootRect.left) + 'px'; this.gameHost.style.top = Math.max(0, r.top - rootRect.top) + 'px'; this.gameHost.style.width = Math.max(1, r.width) + 'px'; this.gameHost.style.height = Math.max(1, r.height) + 'px'; var iframe = document.getElementById('pw-game-iframe'); if (iframe) iframe.style.pointerEvents = 'auto'; }; WorkbenchLayout.prototype._renderNode = function (node) { if (!node) return null; if (node.type === 'split') return this._renderSplit(node); if (node.type === 'game') return this._renderGame(node); return this._renderTabs(node); }; WorkbenchLayout.prototype._renderSplit = function (node) { var self = this; this._ensureId(node); var wrap = document.createElement('div'); wrap.className = 'pw-split-node pw-split-dir-' + (node.dir === 'v' ? 'v' : 'h'); wrap.setAttribute('data-node-id', node.id); this._nodeEls[node.id] = { el: wrap, node: node }; var kids = node.children || []; var sizes = node.sizes || []; this._normalizeSizes(sizes, kids.length); for (var i = 0; i < kids.length; i++) { if (i > 0) { (function (index) { var splitter = document.createElement('div'); splitter.className = 'pw-splitter ' + (node.dir === 'v' ? 'pw-splitter-v' : 'pw-splitter-h'); splitter.addEventListener('pointerdown', function (ev) { self._beginSplitDrag(ev, node, wrap, index - 1); }); wrap.appendChild(splitter); })(i); } var childWrap = document.createElement('div'); childWrap.className = 'pw-split-child'; childWrap.style.flex = String(sizes[i]) + ' 1 0'; childWrap.style.minWidth = '0'; childWrap.style.minHeight = '0'; var childEl = this._renderNode(kids[i]); if (childEl) childWrap.appendChild(childEl); wrap.appendChild(childWrap); } return wrap; }; WorkbenchLayout.prototype._normalizeSizes = function (sizes, n) { while (sizes.length < n) sizes.push(1 / n); sizes.length = n; var sum = 0; for (var i = 0; i < n; i++) sum += sizes[i] || 0; if (sum <= 0) { for (var j = 0; j < n; j++) sizes[j] = 1 / n; return; } for (var k = 0; k < n; k++) sizes[k] = sizes[k] / sum; }; WorkbenchLayout.prototype._beginSplitDrag = function (ev, node, wrap, leftIndex) { ev.preventDefault(); ev.stopPropagation(); var children = []; for (var i = 0; i < wrap.children.length; i++) { if (wrap.children[i].classList.contains('pw-split-child')) children.push(wrap.children[i]); } if (leftIndex < 0 || leftIndex >= children.length - 1) return; var a = children[leftIndex]; var b = children[leftIndex + 1]; this._splitDrag = { node: node, dir: node.dir, leftIndex: leftIndex, startPos: node.dir === 'h' ? ev.clientX : ev.clientY, a0: node.dir === 'h' ? a.getBoundingClientRect().width : a.getBoundingClientRect().height, b0: node.dir === 'h' ? b.getBoundingClientRect().width : b.getBoundingClientRect().height, aEl: a, bEl: b, }; var prevCursor = document.body.style.cursor; document.body.style.cursor = node.dir === 'h' ? 'col-resize' : 'row-resize'; document.body.classList.add('pw-split-dragging'); // iframe 会吞掉 pointermove;拖动期间禁用命中 this._setGameIframePassThrough(true); var target = ev.currentTarget || ev.target; if (target && target.setPointerCapture && ev.pointerId != null) { try { target.setPointerCapture(ev.pointerId); } catch (e) { /* */ } } var self = this; var move = function (e) { self._onSplitMove(e); }; var up = function (e) { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); window.removeEventListener('pointercancel', up); if (target && target.releasePointerCapture && e && e.pointerId != null) { try { target.releasePointerCapture(e.pointerId); } catch (err) { /* */ } } document.body.style.cursor = prevCursor || ''; document.body.classList.remove('pw-split-dragging'); self._setGameIframePassThrough(false); self._splitDrag = null; self._dirty = true; self._save(); self._syncGameHostGeom(); self._notifyViewResize(); }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', up); window.addEventListener('pointercancel', up); }; /** @param {boolean} on */ WorkbenchLayout.prototype._setGameIframePassThrough = function (on) { var iframe = document.getElementById('pw-game-iframe'); if (iframe) iframe.style.pointerEvents = on ? 'none' : ''; var host = this.gameHost; if (host) host.classList.toggle('pw-iframe-passthrough', !!on); }; WorkbenchLayout.prototype._onSplitMove = function (ev) { var d = this._splitDrag; if (!d) return; var delta = (d.dir === 'h' ? ev.clientX : ev.clientY) - d.startPos; var total = d.a0 + d.b0; if (total <= 1) return; var a1 = Math.max(80, Math.min(total - 80, d.a0 + delta)); var b1 = total - a1; var ra = a1 / total; var rb = b1 / total; var baseA = d.node.sizes[d.leftIndex]; var baseB = d.node.sizes[d.leftIndex + 1]; var pair = baseA + baseB; d.node.sizes[d.leftIndex] = pair * ra; d.node.sizes[d.leftIndex + 1] = pair * rb; d.aEl.style.flex = d.node.sizes[d.leftIndex] + ' 1 0'; d.bEl.style.flex = d.node.sizes[d.leftIndex + 1] + ' 1 0'; this._syncGameHostGeom(); }; WorkbenchLayout.prototype._renderGame = function (node) { this._ensureId(node); var leaf = document.createElement('div'); leaf.className = 'pw-leaf pw-leaf-game'; leaf.setAttribute('data-leaf-id', node.id); leaf.setAttribute('data-node-id', node.id); this._leafEls[node.id] = { el: leaf, node: node }; this._nodeEls[node.id] = { el: leaf, node: node }; var ph = document.createElement('div'); ph.className = 'pw-game-placeholder'; ph.setAttribute('data-game-placeholder', '1'); leaf.appendChild(ph); this._gamePlaceholder = ph; return leaf; }; WorkbenchLayout.prototype._renderTabs = function (node) { var self = this; this._ensureId(node); var leaf = document.createElement('div'); leaf.className = 'pw-leaf pw-leaf-tabs'; leaf.setAttribute('data-leaf-id', node.id); leaf.setAttribute('data-node-id', node.id); this._leafEls[node.id] = { el: leaf, node: node }; this._nodeEls[node.id] = { el: leaf, node: node }; var tabs = document.createElement('div'); tabs.className = 'pw-leaf-tabbar'; tabs.setAttribute('data-leaf-id', node.id); var body = document.createElement('div'); body.className = 'pw-leaf-body'; body.setAttribute('data-leaf-id', node.id); var panels = node.panels || []; if (!panels.length) { leaf.appendChild(tabs); leaf.appendChild(body); return leaf; } if (panels.indexOf(node.active) < 0) node.active = panels[0]; for (var i = 0; i < panels.length; i++) { (function (pid) { var tab = document.createElement('button'); tab.type = 'button'; tab.className = 'pw-leaf-tab' + (pid === node.active ? ' active' : ''); tab.textContent = PANEL_TITLES[pid] || pid; tab.setAttribute('data-panel', pid); tab.draggable = true; tab.addEventListener('click', function (ev) { ev.preventDefault(); if (node.active === pid) return; node.active = pid; // 仅切换页签内容,禁止 rebuild(会搬移/重排导致 iframe 重载) self._activateTabLeaf(leaf, tabs, body, node); self._save(); self._emitVisibility(); }); tab.addEventListener('dragstart', function (ev) { self._beginDrag(pid, ev); }); tabs.appendChild(tab); })(panels[i]); } var activeEl = this._panelBodies[node.active]; if (activeEl) body.appendChild(activeEl); leaf.appendChild(tabs); leaf.appendChild(body); return leaf; }; /** * 就地切换页签,不重建停靠树。 * @param {HTMLElement} leaf * @param {HTMLElement} tabs * @param {HTMLElement} body * @param {object} node */ WorkbenchLayout.prototype._activateTabLeaf = function (leaf, tabs, body, node) { var buttons = tabs.querySelectorAll('.pw-leaf-tab'); for (var i = 0; i < buttons.length; i++) { var b = buttons[i]; b.classList.toggle('active', b.getAttribute('data-panel') === node.active); } while (body.firstChild) body.removeChild(body.firstChild); var el = this._panelBodies[node.active]; if (el) body.appendChild(el); }; WorkbenchLayout.prototype._beginDrag = function (panelId, ev) { if (this.mode === 'compact') { ev.preventDefault(); return; } this._dragPanel = panelId; this._hoverGroupCols = null; this._setGameIframePassThrough(true); ev.dataTransfer.setData('text/pw-panel', panelId); ev.dataTransfer.effectAllowed = 'move'; if (this.dropOverlay) this.dropOverlay.hidden = true; }; WorkbenchLayout.prototype._onDocDragEnd = function () { var self = this; this._setGameIframePassThrough(false); // drop 常在 dragend 之后触发,延迟清理避免丢掉落点信息 setTimeout(function () { self._dragPanel = null; self._hoverTargetId = null; self._hoverEdge = null; self._hoverGroup = null; self._hoverGroupCols = null; if (self.dropOverlay) { self.dropOverlay.hidden = true; self.dropOverlay.className = 'pw-drop-overlay'; } }, 0); }; WorkbenchLayout.prototype._clearDragVisual = function () { if (this.dropOverlay) { this.dropOverlay.hidden = true; this.dropOverlay.className = 'pw-drop-overlay'; } }; WorkbenchLayout.prototype._onDocDragOver = function (ev) { if (!this._dragPanel) return; var leafEl = ev.target.closest && ev.target.closest('.pw-leaf'); if (!leafEl) { if (this.dropOverlay) this.dropOverlay.hidden = true; return; } ev.preventDefault(); var leafId = leafEl.getAttribute('data-leaf-id'); var info = this._leafEls[leafId]; if (!info) return; var rect = leafEl.getBoundingClientRect(); var x = (ev.clientX - rect.left) / Math.max(1, rect.width); var y = (ev.clientY - rect.top) / Math.max(1, rect.height); var edge = this._hitEdge(info.node, x, y, ev.target); var resolved = this._resolveDropTarget(info.node, edge); this._hoverTargetId = resolved.node ? resolved.node.id : null; this._hoverEdge = resolved.edge; this._hoverGroup = resolved.group || null; this._hoverGroupCols = (resolved.group && resolved.group.length) ? this._snapshotGroupColumns(resolved.group, this._dragPanel) : null; if (resolved.group && resolved.group.length) { this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false); } else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') { var splitInfo = this._nodeEls[resolved.node.id]; if (splitInfo && splitInfo.el) { var sr = splitInfo.el.getBoundingClientRect(); var rootRect = this.root.getBoundingClientRect(); this._showOverlayRect(this._edgeStripRect({ left: sr.left - rootRect.left, top: sr.top - rootRect.top, width: sr.width, height: sr.height, }, resolved.edge), 'region', false); } else { this._showOverlay(leafEl, resolved.edge, false); } } else { var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null; var targetEl = targetInfo ? targetInfo.el : leafEl; this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game'); } }; /** 取目标矩形某一侧的新建区预览条带 */ WorkbenchLayout.prototype._edgeStripRect = function (rect, edge) { var r = { left: rect.left, top: rect.top, width: rect.width, height: rect.height }; var ratio = 0.36; if (edge === 'bottom') { r.top = rect.top + rect.height * (1 - ratio); r.height = rect.height * ratio; } else if (edge === 'top') { r.height = rect.height * ratio; } else if (edge === 'left') { r.width = rect.width * ratio; } else if (edge === 'right') { r.left = rect.left + rect.width * (1 - ratio); r.width = rect.width * ratio; } return r; }; WorkbenchLayout.prototype._isGameNode = function (node) { return !!(node && node.type === 'game'); }; WorkbenchLayout.prototype._splitHasGame = function (split) { if (!split || split.type !== 'split') return false; for (var i = 0; i < split.children.length; i++) { if (this._isGameNode(split.children[i])) return true; if (split.children[i].type === 'split' && this._splitHasGame(split.children[i])) return true; } return false; }; /** * 取横/竖排中与 leaf 相邻的连续「非游戏」兄弟,用于跨多列/多行建区且不吞掉游戏区。 */ WorkbenchLayout.prototype._contiguousNonGameSiblings = function (parent, leafIndex) { var kids = parent.children || []; var start = leafIndex; var end = leafIndex; while (start > 0 && !this._isGameNode(kids[start - 1])) start--; while (end < kids.length - 1 && !this._isGameNode(kids[end + 1])) end++; var group = []; for (var i = start; i <= end; i++) group.push(kids[i]); return { start: start, end: end, nodes: group }; }; WorkbenchLayout.prototype._unionNodeRects = function (nodes) { var rootRect = this.root.getBoundingClientRect(); var left = Infinity; var top = Infinity; var right = -Infinity; var bottom = -Infinity; for (var i = 0; i < nodes.length; i++) { this._ensureId(nodes[i]); var info = this._nodeEls[nodes[i].id]; if (!info || !info.el) continue; var r = info.el.getBoundingClientRect(); left = Math.min(left, r.left); top = Math.min(top, r.top); right = Math.max(right, r.right); bottom = Math.max(bottom, r.bottom); } if (!isFinite(left)) { return { left: 0, top: 0, width: 0, height: 0 }; } return { left: left - rootRect.left, top: top - rootRect.top, width: right - left, height: bottom - top, }; }; /** * 边沿停靠解析: * - 标题中心 → 合并页签 * - 横排含游戏区时的上/下 → 只包住连续非游戏兄弟(红框范围),不横跨游戏 * - 竖排含游戏区时的左/右 → 同上 * - 不含游戏的横排上/下 → 提到该横排父级整行 */ WorkbenchLayout.prototype._resolveDropTarget = function (leafNode, edge) { if (edge === 'center') return { node: leafNode, edge: edge, group: null }; var parentInfo = this._findParent(this._tree, leafNode); if (!parentInfo || !parentInfo.parent || parentInfo.parent.type !== 'split') { this._ensureId(leafNode); return { node: leafNode, edge: edge, group: null }; } var parent = parentInfo.parent; var index = parentInfo.index; // 横排 + 上/下:若同排有游戏,只圈选连续非游戏列 if ((edge === 'top' || edge === 'bottom') && parent.dir === 'h') { if (this._splitHasGame(parent) || this._parentRowHasGameSibling(parent)) { var g = this._contiguousNonGameSiblings(parent, index); if (g.nodes.length >= 1) { return { node: null, edge: edge, group: g.nodes, groupParent: parent, groupStart: g.start, groupEnd: g.end }; } } this._ensureId(parent); return { node: parent, edge: edge, group: null }; } // 竖排 + 左/右:若同列有游戏,只圈选连续非游戏行 if ((edge === 'left' || edge === 'right') && parent.dir === 'v') { if (this._splitHasGame(parent) || this._parentRowHasGameSibling(parent)) { var g2 = this._contiguousNonGameSiblings(parent, index); if (g2.nodes.length >= 1) { return { node: null, edge: edge, group: g2.nodes, groupParent: parent, groupStart: g2.start, groupEnd: g2.end }; } } this._ensureId(parent); return { node: parent, edge: edge, group: null }; } // 外侧边:提升到父级(仍避开把游戏一并包进「整行上下」) if (parent.dir === 'h' && edge === 'left' && index === 0) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } if (parent.dir === 'h' && edge === 'right' && index === parent.children.length - 1) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } if (parent.dir === 'v' && edge === 'top' && index === 0) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } if (parent.dir === 'v' && edge === 'bottom' && index === parent.children.length - 1) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } this._ensureId(leafNode); return { node: leafNode, edge: edge, group: null }; }; WorkbenchLayout.prototype._parentRowHasGameSibling = function (split) { // 父级自身子节点含 game for (var i = 0; i < split.children.length; i++) { if (this._isGameNode(split.children[i])) return true; } return false; }; WorkbenchLayout.prototype._hitEdge = function (node, x, y, target) { // 标题栏优先:合并 if (target && target.closest && target.closest('.pw-leaf-tabbar, .pw-leaf-tab')) { return 'center'; } var m = 0.22; var onLeft = x < m; var onRight = x > 1 - m; var onTop = y < m; var onBottom = y > 1 - m; // 角落按更近边 if (onLeft && onTop) return x < y ? 'left' : 'top'; if (onRight && onTop) return (1 - x) < y ? 'right' : 'top'; if (onLeft && onBottom) return x < (1 - y) ? 'left' : 'bottom'; if (onRight && onBottom) return (1 - x) < (1 - y) ? 'right' : 'bottom'; if (onLeft) return 'left'; if (onRight) return 'right'; if (onTop) return 'top'; if (onBottom) return 'bottom'; // 游戏区域中心不允许合并页签,默认下方提示 if (node.type === 'game') return 'bottom'; return 'center'; }; WorkbenchLayout.prototype._showOverlay = function (leafEl, edge, isGame) { var rect = leafEl.getBoundingClientRect(); var rootRect = this.root.getBoundingClientRect(); this._showOverlayRect({ left: rect.left - rootRect.left, top: rect.top - rootRect.top, width: rect.width, height: rect.height, }, edge, isGame); }; WorkbenchLayout.prototype._showOverlayRect = function (rect, edge, isGame) { var overlay = this.dropOverlay; if (!overlay) return; overlay.hidden = false; overlay.style.left = rect.left + 'px'; overlay.style.top = rect.top + 'px'; overlay.style.width = Math.max(0, rect.width) + 'px'; overlay.style.height = Math.max(0, rect.height) + 'px'; var isRegion = edge === 'region'; overlay.className = 'pw-drop-overlay edge-' + (isRegion ? 'center' : edge) + (isGame ? ' is-game' : '') + (isRegion ? ' is-region' : ''); var center = overlay.querySelector('.pw-drop-e-center'); if (center) { center.hidden = !!isGame; center.textContent = isRegion ? '在此新建区域' : '合并到此区域'; } }; WorkbenchLayout.prototype._onDocDrop = function (ev) { var panelId = ev.dataTransfer.getData('text/pw-panel') || this._dragPanel; var targetId = this._hoverTargetId; var edge = this._hoverEdge; var group = this._hoverGroup; var groupCols = this._hoverGroupCols; this._clearDragVisual(); this._dragPanel = null; this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; if (!panelId || !edge) return; if (!targetId && !(groupCols && groupCols.length) && !(group && group.length)) return; ev.preventDefault(); this._applyDrop(panelId, targetId, edge, group, groupCols); }; WorkbenchLayout.prototype._applyDrop = function (panelId, targetId, edge, group, groupCols) { this._closeFloat(panelId, false); // 组停靠:用「列内容快照」而非节点引用,避免 remove/cleanup 后 id 失效导致只包一列 if (edge !== 'center' && ((groupCols && groupCols.length) || (group && group.length))) { var cols = (groupCols && groupCols.length) ? groupCols : this._snapshotGroupColumns(group, panelId); this._removePanel(panelId); var nodes = this._resolveColumnsFromSnapshot(cols); if (nodes.length >= 1) { this._splitBesideGroup(nodes, edge, panelId); } else { this._openFloat(panelId, null); } this._dirty = true; this._tree = this._cleanup(this._tree) || this._presetTree('minimal'); this.rebuild(); this._rememberPanelFromTree(panelId); this._save(); return; } var target = this._findNodeById(this._tree, targetId); if (!target) return; if (target.type === 'tabs' && (target.panels || []).indexOf(panelId) >= 0 && edge === 'center') { target.active = panelId; this.rebuild(); this._save(); return; } this._removePanel(panelId); target = this._findNodeById(this._tree, targetId); if (!target) { this._openFloat(panelId, null); this._dirty = true; this._save(); return; } if (edge === 'center') { if (target.type === 'game' || target.type === 'split') { this._splitBeside(target, 'bottom', panelId); } else if (target.type === 'tabs') { if (target.panels.indexOf(panelId) < 0) target.panels.push(panelId); target.active = panelId; } } else { this._splitBeside(target, edge, panelId); } this._dirty = true; this._tree = this._cleanup(this._tree) || this._presetTree('minimal'); this.rebuild(); this._rememberPanelFromTree(panelId); this._save(); }; /** 组停靠前快照:记录各列在排除拖拽面板后仍保留的 panelId */ WorkbenchLayout.prototype._snapshotGroupColumns = function (group, excludePanelId) { var cols = []; for (var i = 0; i < group.length; i++) { var node = group[i]; if (this._isGameNode(node)) { cols.push({ kind: 'game' }); continue; } var panels = this._collectPanelIds(node); var kept = []; for (var j = 0; j < panels.length; j++) { if (panels[j] !== excludePanelId) kept.push(panels[j]); } if (!kept.length) continue; cols.push({ kind: 'panels', panels: kept }); } return cols; }; WorkbenchLayout.prototype._collectPanelIds = function (node) { var out = []; this._walkPanelIds(node, out); return out; }; WorkbenchLayout.prototype._walkPanelIds = function (node, out) { if (!node) return; if (node.type === 'tabs') { var panels = node.panels || []; for (var i = 0; i < panels.length; i++) out.push(panels[i]); return; } if (node.type === 'split') { for (var j = 0; j < node.children.length; j++) this._walkPanelIds(node.children[j], out); } }; /** 按快照在 cleanup 后的树中找回同级列节点 */ WorkbenchLayout.prototype._resolveColumnsFromSnapshot = function (cols) { var leaves = []; for (var i = 0; i < cols.length; i++) { if (cols[i].kind === 'game') { var g = this._findNodeById(this._tree, 'leaf_game'); if (g) leaves.push(g); continue; } var panels = cols[i].panels || []; var found = null; for (var p = 0; p < panels.length; p++) { found = this._findTabsWith(this._tree, panels[p]); if (found) break; } if (found && leaves.indexOf(found) < 0) leaves.push(found); } return this._liftToSiblingBundle(leaves); }; WorkbenchLayout.prototype._pathNodes = function (target) { var path = []; var walk = function (node, acc) { if (node === target) { path = acc.concat([node]); return true; } if (node && node.type === 'split') { for (var i = 0; i < node.children.length; i++) { if (walk(node.children[i], acc.concat([node]))) return true; } } return false; }; walk(this._tree, []); return path; }; WorkbenchLayout.prototype._containsNode = function (root, target) { if (root === target) return true; if (!root || root.type !== 'split') return false; for (var i = 0; i < root.children.length; i++) { if (this._containsNode(root.children[i], target)) return true; } return false; }; WorkbenchLayout.prototype._directChildContaining = function (parent, target) { if (!parent || parent.type !== 'split') return null; for (var i = 0; i < parent.children.length; i++) { if (this._containsNode(parent.children[i], target)) return parent.children[i]; } return null; }; /** 将多个叶子提升为同一父 split 下的直接子节点(保持顺序) */ WorkbenchLayout.prototype._liftToSiblingBundle = function (leaves) { if (!leaves || !leaves.length) return []; if (leaves.length === 1) return leaves; var paths = []; for (var i = 0; i < leaves.length; i++) paths.push(this._pathNodes(leaves[i])); var parent = null; var depth = 0; while (true) { var ref = paths[0][depth]; if (!ref) break; var same = true; for (var a = 1; a < paths.length; a++) { if (paths[a][depth] !== ref) { same = false; break; } } if (!same) break; parent = ref; depth++; } if (!parent || parent.type !== 'split') return leaves; var bundle = []; var seen = {}; for (var L = 0; L < leaves.length; L++) { var child = this._directChildContaining(parent, leaves[L]); if (child && child.id && !seen[child.id]) { seen[child.id] = true; bundle.push(child); } } bundle.sort(function (a, b) { return parent.children.indexOf(a) - parent.children.indexOf(b); }); return bundle.length ? bundle : leaves; }; /** * 将一组兄弟节点包成整体,再在指定边沿外侧新建区域。 * 例:h[层级,属性,游戏] 底边 → h[ v[h[层级,属性], 控制台], 游戏] */ WorkbenchLayout.prototype._splitBesideGroup = function (nodes, edge, panelId) { if (!nodes || !nodes.length) return; var first = nodes[0]; var parentInfo = this._findParent(this._tree, first); if (!parentInfo || !parentInfo.parent) { this._splitBeside(first, edge, panelId); return; } var parent = parentInfo.parent; var start = parentInfo.index; // 校验 nodes 仍是 parent 中连续片段(允许 id 匹配) for (var i = 0; i < nodes.length; i++) { var child = parent.children[start + i]; if (!child || (child !== nodes[i] && child.id !== nodes[i].id)) { this._splitBeside(first, edge, panelId); return; } nodes[i] = child; } var end = start + nodes.length - 1; var newLeaf = this._tabsNode([panelId]); var dir = (edge === 'left' || edge === 'right') ? 'h' : 'v'; var innerDir = parent.dir; var groupNode; if (nodes.length === 1) { groupNode = nodes[0]; } else { var groupSizes = []; for (var s = start; s <= end; s++) { groupSizes.push(parent.sizes[s] != null ? parent.sizes[s] : 1); } this._normalizeSizes(groupSizes, groupSizes.length); groupNode = { type: 'split', id: 'split_' + (this._leafSeq++), dir: innerDir, sizes: groupSizes, children: nodes.slice(), }; } var wrapped = { type: 'split', id: 'split_' + (this._leafSeq++), dir: dir, sizes: [0.64, 0.36], children: (edge === 'left' || edge === 'top') ? [newLeaf, groupNode] : [groupNode, newLeaf], }; var newChildren = []; var newSizes = []; for (var c = 0; c < parent.children.length; c++) { if (c === start) { newChildren.push(wrapped); var sum = 0; for (var k = start; k <= end; k++) sum += parent.sizes[k] != null ? parent.sizes[k] : 0; newSizes.push(sum > 0 ? sum : 1); c = end; continue; } if (c < start || c > end) { newChildren.push(parent.children[c]); newSizes.push(parent.sizes[c] != null ? parent.sizes[c] : 1); } } parent.children = newChildren; parent.sizes = newSizes; this._normalizeSizes(parent.sizes, parent.children.length); }; WorkbenchLayout.prototype._splitBeside = function (targetNode, edge, panelId) { this._ensureId(targetNode); var parentInfo = this._findParent(this._tree, targetNode); var newLeaf = this._tabsNode([panelId]); var dir = (edge === 'left' || edge === 'right') ? 'h' : 'v'; var first = (edge === 'left' || edge === 'top') ? newLeaf : targetNode; var second = (edge === 'left' || edge === 'top') ? targetNode : newLeaf; if (!parentInfo) { this._tree = { type: 'split', id: 'split_' + (this._leafSeq++), dir: dir, sizes: [0.5, 0.5], children: [first, second], }; return; } var parent = parentInfo.parent; var index = parentInfo.index; this._ensureId(parent); // 同方向则插入为兄弟;否则把整个目标(可为 split 整行/整列)包进新 split if (parent.type === 'split' && parent.dir === dir) { var insertAt = (edge === 'left' || edge === 'top') ? index : index + 1; parent.children.splice(insertAt, 0, newLeaf); var n = parent.children.length; var sizes = []; for (var i = 0; i < n; i++) sizes.push(1 / n); parent.sizes = sizes; } else { var split = { type: 'split', id: 'split_' + (this._leafSeq++), dir: dir, sizes: [0.5, 0.5], children: [first, second], }; parent.children[index] = split; } }; WorkbenchLayout.prototype._removePanel = function (panelId) { this._removePanelFrom(this._tree, panelId); this._tree = this._cleanup(this._tree); }; WorkbenchLayout.prototype._removePanelFrom = function (node, panelId) { if (!node) return false; if (node.type === 'tabs') { var idx = (node.panels || []).indexOf(panelId); if (idx >= 0) { node.panels.splice(idx, 1); if (node.active === panelId) node.active = node.panels[0] || ''; return true; } return false; } if (node.type === 'split') { for (var i = 0; i < node.children.length; i++) { if (this._removePanelFrom(node.children[i], panelId)) return true; } } return false; }; WorkbenchLayout.prototype._cleanup = function (node) { if (!node) return null; if (node.type === 'game') return node; if (node.type === 'tabs') { if (!node.panels || !node.panels.length) return null; return node; } if (node.type === 'split') { var kids = []; var sizes = []; for (var i = 0; i < node.children.length; i++) { var c = this._cleanup(node.children[i]); if (c) { kids.push(c); sizes.push(node.sizes && node.sizes[i] != null ? node.sizes[i] : 1); } } if (!kids.length) return null; if (kids.length === 1) return kids[0]; // 合并同方向嵌套 var flatKids = []; var flatSizes = []; for (var j = 0; j < kids.length; j++) { if (kids[j].type === 'split' && kids[j].dir === node.dir) { var sub = kids[j]; var part = sizes[j]; var subSum = 0; for (var s = 0; s < sub.sizes.length; s++) subSum += sub.sizes[s]; for (var k = 0; k < sub.children.length; k++) { flatKids.push(sub.children[k]); flatSizes.push(part * (sub.sizes[k] / (subSum || 1))); } } else { flatKids.push(kids[j]); flatSizes.push(sizes[j]); } } this._normalizeSizes(flatSizes, flatKids.length); return { type: 'split', dir: node.dir, sizes: flatSizes, children: flatKids }; } return node; }; WorkbenchLayout.prototype._findNodeById = function (node, id) { if (!node) return null; if (node.id === id) return node; if (node.type === 'split') { for (var i = 0; i < node.children.length; i++) { var f = this._findNodeById(node.children[i], id); if (f) return f; } } return null; }; WorkbenchLayout.prototype._findParent = function (node, child, parent, index) { if (!node) return null; if (node === child) return parent ? { parent: parent, index: index } : null; if (node.type === 'split') { for (var i = 0; i < node.children.length; i++) { var f = this._findParent(node.children[i], child, node, i); if (f) return f; } } return null; }; WorkbenchLayout.prototype._findTabsWith = function (node, panelId) { if (!node) return null; if (node.type === 'tabs' && (node.panels || []).indexOf(panelId) >= 0) return node; if (node.type === 'split') { for (var i = 0; i < node.children.length; i++) { var f = this._findTabsWith(node.children[i], panelId); if (f) return f; } } return null; }; WorkbenchLayout.prototype.togglePanel = function (panelId) { if (this.mode === 'compact') { if (this.activeDrawer === panelId) this.closeDrawer(); else this.openDrawer(panelId); return; } if (this._floats[panelId]) { this._rememberFloat(panelId); this._closeFloat(panelId, true); this._savePanelMemory(); this._emitVisibility(); return; } var existing = this._findTabsWith(this._tree, panelId); if (existing) { if (existing.panels.length === 1 && existing.active === panelId) { this._rememberPanelFromTree(panelId); this._removePanel(panelId); this._tree = this._cleanup(this._tree) || this._presetTree('minimal'); this._dirty = true; this.rebuild(); this._save(); return; } existing.active = panelId; this.rebuild(); this._save(); return; } // 打开:优先记忆位置;无记忆则悬浮,禁止强行改当前停靠树 this._openPanelByMemory(panelId); }; WorkbenchLayout.prototype._openPanelByMemory = function (panelId) { var mem = this._panelMemory[panelId]; if (mem && mem.mode === 'tabs' && mem.mates && mem.mates.length) { for (var i = 0; i < mem.mates.length; i++) { var host = this._findTabsWith(this._tree, mem.mates[i]); if (host) { if (host.panels.indexOf(panelId) < 0) host.panels.push(panelId); host.active = panelId; this._dirty = true; this.rebuild(); this._save(); return; } } } if (mem && mem.mode === 'dock' && mem.anchors && mem.anchors.length) { var leaves = []; for (var a = 0; a < mem.anchors.length; a++) { var leaf = this._findTabsWith(this._tree, mem.anchors[a]); if (leaf && leaves.indexOf(leaf) < 0) leaves.push(leaf); } var bundle = this._liftToSiblingBundle(leaves); if (bundle.length) { if (bundle.length === 1) this._splitBeside(bundle[0], mem.edge || 'bottom', panelId); else this._splitBesideGroup(bundle, mem.edge || 'bottom', panelId); this._dirty = true; this._tree = this._cleanup(this._tree) || this._tree; this.rebuild(); this._rememberPanelFromTree(panelId); this._save(); return; } } this._openFloat(panelId, mem && mem.float ? mem.float : null); this._savePanelMemory(); }; WorkbenchLayout.prototype._rememberPanelFromTree = function (panelId) { var leaf = this._findTabsWith(this._tree, panelId); if (!leaf) return; if ((leaf.panels || []).length > 1) { var mates = []; for (var i = 0; i < leaf.panels.length; i++) { if (leaf.panels[i] !== panelId) mates.push(leaf.panels[i]); } this._panelMemory[panelId] = { mode: 'tabs', mates: mates }; this._savePanelMemory(); return; } var parentInfo = this._findParent(this._tree, leaf); var floatRect = null; var info = this._leafEls[leaf.id]; if (info && info.el) { var r = info.el.getBoundingClientRect(); var rootRect = this.root.getBoundingClientRect(); floatRect = { x: Math.max(8, r.left - rootRect.left), y: Math.max(40, r.top - rootRect.top), w: Math.max(240, r.width), h: Math.max(160, r.height), }; } if (!parentInfo || !parentInfo.parent) { this._panelMemory[panelId] = { mode: 'float', float: floatRect }; this._savePanelMemory(); return; } var parent = parentInfo.parent; var idx = parentInfo.index; var neighbor = idx > 0 ? parent.children[idx - 1] : parent.children[idx + 1]; var edge = 'bottom'; if (parent.dir === 'h') edge = idx > 0 ? 'right' : 'left'; else edge = idx > 0 ? 'bottom' : 'top'; var anchors = neighbor ? this._collectPanelIds(neighbor) : []; this._panelMemory[panelId] = { mode: anchors.length ? 'dock' : 'float', anchors: anchors, edge: edge, float: floatRect, }; this._savePanelMemory(); }; WorkbenchLayout.prototype._ensureFloatLayer = function () { if (this._floatLayer) return; var layer = document.createElement('div'); layer.className = 'pw-float-layer'; layer.id = 'pw-float-layer'; this.root.appendChild(layer); this._floatLayer = layer; }; WorkbenchLayout.prototype._openFloat = function (panelId, rect) { this._ensureFloatLayer(); if (this._floats[panelId]) return; var body = this._panelBodies[panelId]; if (!body) return; var win = document.createElement('div'); win.className = 'pw-float-win'; win.setAttribute('data-float-panel', panelId); var x = (rect && rect.x != null) ? rect.x : 72 + Object.keys(this._floats).length * 24; var y = (rect && rect.y != null) ? rect.y : 72 + Object.keys(this._floats).length * 24; var w = (rect && rect.w != null) ? rect.w : 360; var h = (rect && rect.h != null) ? rect.h : 320; win.style.left = x + 'px'; win.style.top = y + 'px'; win.style.width = w + 'px'; win.style.height = h + 'px'; var head = document.createElement('div'); head.className = 'pw-float-head'; head.innerHTML = '' + '拖标题可停靠' + ''; head.querySelector('.pw-float-title').textContent = PANEL_TITLES[panelId] || panelId; var content = document.createElement('div'); content.className = 'pw-float-body'; content.appendChild(body); win.appendChild(head); win.appendChild(content); this._floatLayer.appendChild(win); var self = this; head.querySelector('.pw-float-close').addEventListener('click', function () { self._rememberFloat(panelId); self._closeFloat(panelId, true); self._savePanelMemory(); }); // 标题文字:HTML5 拖放到布局区停靠 var title = head.querySelector('.pw-float-title'); title.addEventListener('dragstart', function (ev) { self._floatDrag = null; win.classList.add('pw-float-dock-dragging'); self._beginDrag(panelId, ev); }); title.addEventListener('dragend', function () { win.classList.remove('pw-float-dock-dragging'); self._clearDragVisual(); self._dragPanel = null; self._hoverTargetId = null; self._hoverEdge = null; self._hoverGroup = null; self._hoverGroupCols = null; }); // 标题栏空白处:移动悬浮窗位置(用当前坐标,禁止用创建时闭包坐标) head.addEventListener('pointerdown', function (ev) { if (ev.target && ev.target.closest && ev.target.closest('.pw-float-close')) return; if (ev.target && ev.target.closest && ev.target.closest('.pw-float-title')) return; var f = self._floats[panelId]; if (!f) return; ev.preventDefault(); head.setPointerCapture && head.setPointerCapture(ev.pointerId); self._setGameIframePassThrough(true); self._floatDrag = { id: panelId, ox: ev.clientX, oy: ev.clientY, x: f.x, y: f.y, moved: false, }; window.addEventListener('pointermove', self._onFloatMove); window.addEventListener('pointerup', self._onFloatUp); }); this._floats[panelId] = { x: x, y: y, w: w, h: h, el: win }; this._panelMemory[panelId] = { mode: 'float', float: { x: x, y: y, w: w, h: h } }; }; WorkbenchLayout.prototype._onFloatMove = function (ev) { var d = this._floatDrag; if (!d || !this._floats[d.id]) return; var nx = d.x + (ev.clientX - d.ox); var ny = d.y + (ev.clientY - d.oy); var f = this._floats[d.id]; var rootRect = this.root.getBoundingClientRect(); f.x = Math.max(0, Math.min(rootRect.width - 80, nx)); f.y = Math.max(28, Math.min(rootRect.height - 40, ny)); f.el.style.left = f.x + 'px'; f.el.style.top = f.y + 'px'; d.moved = true; // 移动时探测下方停靠区(标题拖放之外的快捷停靠) this._probeFloatDockHover(ev.clientX, ev.clientY, d.id); }; WorkbenchLayout.prototype._onFloatUp = function (ev) { var d = this._floatDrag; this._floatDrag = null; window.removeEventListener('pointermove', this._onFloatMove); window.removeEventListener('pointerup', this._onFloatUp); this._setGameIframePassThrough(false); if (!d) return; var docked = false; if (d.moved && this._hoverEdge && (this._hoverTargetId || (this._hoverGroupCols && this._hoverGroupCols.length))) { docked = this._dockFloatingPanel(d.id, ev.clientX, ev.clientY); } this._clearDragVisual(); this._dragPanel = null; this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; if (!docked) { this._rememberFloat(d.id); this._savePanelMemory(); } }; /** 悬浮窗移动时:临时穿透命中下方 leaf,刷新停靠指示 */ WorkbenchLayout.prototype._probeFloatDockHover = function (cx, cy, panelId) { var floats = this._floats; var ids = []; for (var id in floats) { if (Object.prototype.hasOwnProperty.call(floats, id) && floats[id].el) { ids.push(id); floats[id].el.style.pointerEvents = 'none'; } } var el = document.elementFromPoint(cx, cy); for (var i = 0; i < ids.length; i++) { if (floats[ids[i]] && floats[ids[i]].el) floats[ids[i]].el.style.pointerEvents = ''; } var leafEl = el && el.closest && el.closest('.pw-leaf'); if (!leafEl) { this._clearDragVisual(); this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; return; } this._dragPanel = panelId; var leafId = leafEl.getAttribute('data-leaf-id'); var info = this._leafEls[leafId]; if (!info) return; var rect = leafEl.getBoundingClientRect(); var x = (cx - rect.left) / Math.max(1, rect.width); var y = (cy - rect.top) / Math.max(1, rect.height); var edge = this._hitEdge(info.node, x, y, el); var resolved = this._resolveDropTarget(info.node, edge); this._hoverTargetId = resolved.node ? resolved.node.id : null; this._hoverEdge = resolved.edge; this._hoverGroup = resolved.group || null; this._hoverGroupCols = (resolved.group && resolved.group.length) ? this._snapshotGroupColumns(resolved.group, panelId) : null; if (resolved.group && resolved.group.length) { this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false); } else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') { var splitInfo = this._nodeEls[resolved.node.id]; if (splitInfo && splitInfo.el) { var sr = splitInfo.el.getBoundingClientRect(); var rootRect = this.root.getBoundingClientRect(); this._showOverlayRect(this._edgeStripRect({ left: sr.left - rootRect.left, top: sr.top - rootRect.top, width: sr.width, height: sr.height, }, resolved.edge), 'region', false); } } else { var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null; var targetEl = targetInfo ? targetInfo.el : leafEl; this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game'); } }; WorkbenchLayout.prototype._dockFloatingPanel = function (panelId, cx, cy) { this._probeFloatDockHover(cx, cy, panelId); var targetId = this._hoverTargetId; var edge = this._hoverEdge; var group = this._hoverGroup; var groupCols = this._hoverGroupCols; if (!edge) return false; if (!targetId && !(groupCols && groupCols.length) && !(group && group.length)) return false; this._applyDrop(panelId, targetId, edge, group, groupCols); return !this._floats[panelId]; }; WorkbenchLayout.prototype._rememberFloat = function (panelId) { var f = this._floats[panelId]; if (!f) return; this._panelMemory[panelId] = { mode: 'float', float: { x: f.x, y: f.y, w: f.w, h: f.h }, }; }; WorkbenchLayout.prototype._closeFloat = function (panelId, keepMemory) { var f = this._floats[panelId]; if (!f) return; if (f.el && f.el.parentNode) f.el.parentNode.removeChild(f.el); delete this._floats[panelId]; if (!keepMemory) return; }; WorkbenchLayout.prototype._restoreFloats = function () { if (this._workbenchOff) return; for (var id in this._panelMemory) { if (!Object.prototype.hasOwnProperty.call(this._panelMemory, id)) continue; var mem = this._panelMemory[id]; if (mem && mem.mode === 'float' && !this._findTabsWith(this._tree, id)) { this._openFloat(id, mem.float || null); } } }; WorkbenchLayout.prototype._savePanelMemory = function () { try { localStorage.setItem(PANEL_MEM_KEY, JSON.stringify(this._panelMemory)); } catch (e) { /* */ } }; WorkbenchLayout.prototype._loadPanelMemory = function () { try { var raw = localStorage.getItem(PANEL_MEM_KEY); if (!raw) return; var data = JSON.parse(raw); if (data && typeof data === 'object') this._panelMemory = data; } catch (e) { this._panelMemory = {}; } }; WorkbenchLayout.prototype._findFirstTabs = function (node) { if (!node) return null; if (node.type === 'tabs') return node; if (node.type === 'split') { for (var i = 0; i < node.children.length; i++) { var f = this._findFirstTabs(node.children[i]); if (f) return f; } } return null; }; WorkbenchLayout.prototype.openDrawer = function (panelId) { var drawer = document.getElementById('pw-drawer'); var body = document.getElementById('pw-drawer-body'); var title = document.getElementById('pw-drawer-title'); var el = this._panelBodies[panelId]; if (!drawer || !body || !el) return; body.innerHTML = ''; body.appendChild(el); if (title) title.textContent = PANEL_TITLES[panelId] || panelId; drawer.hidden = false; this.activeDrawer = panelId; this._emitVisibility(); }; WorkbenchLayout.prototype.closeDrawer = function () { var drawer = document.getElementById('pw-drawer'); if (drawer) drawer.hidden = true; var id = this.activeDrawer; this.activeDrawer = ''; if (id) this.rebuild(); else this._emitVisibility(); }; WorkbenchLayout.prototype._bindToolbar = function () { var self = this; var preset = document.getElementById('pw-layout-preset'); if (preset) { preset.value = this.preset; preset.addEventListener('change', function () { self.applyPreset(preset.value, true); }); } var reset = document.getElementById('pw-layout-reset'); if (reset) reset.addEventListener('click', function () { self.resetLayout(); }); var buttons = this.root.querySelectorAll('.pw-panel-btn'); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function (ev) { self.togglePanel(ev.currentTarget.getAttribute('data-panel')); }); } var close = document.getElementById('pw-drawer-close'); if (close) close.addEventListener('click', function () { self.closeDrawer(); }); }; WorkbenchLayout.prototype._onWindowResize = function () { this.refreshMode(); if (this.mode === 'desktop' && !this.workspace.firstChild) this.rebuild(); this._syncGameHostGeom(); this._notifyViewResize(); }; /** * 一键开关工作台:关闭时备份布局并只留游戏与设备选项,开启时还原。 * @param {boolean} on */ WorkbenchLayout.prototype.setWorkbenchEnabled = function (on) { on = !!on; if (on === !this._workbenchOff) return; this._workbenchOff = !on; this.root.classList.toggle('pw-workbench-off', this._workbenchOff); if (this._workbenchOff) { try { this._treeBeforeOff = JSON.parse(JSON.stringify(this._tree)); } catch (e) { this._treeBeforeOff = this._tree; } // 隐藏悬浮窗但不销毁,便于恢复 for (var fid in this._floats) { if (!Object.prototype.hasOwnProperty.call(this._floats, fid)) continue; var f = this._floats[fid]; if (f && f.el) f.el.hidden = true; } this.closeDrawer(); this._tree = this._gameNode(); this.rebuild(); } else { for (var id in this._floats) { if (!Object.prototype.hasOwnProperty.call(this._floats, id)) continue; var fl = this._floats[id]; if (fl && fl.el) fl.el.hidden = false; } if (this._treeBeforeOff) { this._tree = this._treeBeforeOff; this._treeBeforeOff = null; this._reseedLeafIds(this._tree); } else { this._tree = this._presetTree(this.preset || 'analyze'); } this.rebuild(); } }; /** @returns {boolean} */ WorkbenchLayout.prototype.isWorkbenchEnabled = function () { return !this._workbenchOff; }; WorkbenchLayout.prototype._save = function () { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ preset: this.preset, tree: this._tree, dirty: !!this._dirty, })); this._savePanelMemory(); } catch (e) { console.warn('[调试工作台] 布局保存失败', e); } }; WorkbenchLayout.prototype._load = function () { this._loadPanelMemory(); try { var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; var data = JSON.parse(raw); if (data.preset) this.preset = data.preset; if (data.tree) { this._tree = data.tree; this._reseedLeafIds(this._tree); this._dirty = !!data.dirty; } } catch (e) { console.warn('[调试工作台] 布局读取失败', e); this._tree = null; } }; WorkbenchLayout.prototype._reseedLeafIds = function (node) { if (!node) return; if (node.type === 'tabs' || node.type === 'game') { if (!node.id) node.id = node.type === 'game' ? 'leaf_game' : ('leaf_' + (this._leafSeq++)); if (node.type === 'game') node.id = 'leaf_game'; var n = parseInt(String(node.id).replace(/\D/g, ''), 10); if (!isNaN(n) && n >= this._leafSeq) this._leafSeq = n + 1; } if (node.type === 'split') { for (var i = 0; i < node.children.length; i++) this._reseedLeafIds(node.children[i]); } }; global.PWLayout = WorkbenchLayout; })(window);