You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1643 lines
64 KiB

(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.<string, object>} 面板开关记忆 */
this._panelMemory = {};
/** @type {Object.<string, {x:number,y:number,w:number,h:number,el?:HTMLElement}>} */
this._floats = {};
this._floatLayer = null;
this._floatDrag = 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);
}
};
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') {
this._tree = this._presetTree('analyze');
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;
// 先卸下面板与游戏宿主,避免被清空销毁(悬浮窗内的面板保留)
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);
}
if (this.gameHost && this.gameHost.parentNode) {
this.root.appendChild(this.gameHost);
this.gameHost.hidden = true;
}
this._leafEls = {};
this._nodeEls = {};
while (this.workspace.firstChild) this.workspace.removeChild(this.workspace.firstChild);
if (this.mode === 'compact') {
if (this.gameHost) {
this.gameHost.hidden = false;
this.gameHost.classList.add('pw-game-compact');
this.workspace.appendChild(this.gameHost);
}
this._notifyViewResize();
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);
}
this._notifyViewResize();
};
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-' + (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();
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 self = this;
var move = function (e) { self._onSplitMove(e); };
var up = function () {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
self._splitDrag = null;
self._dirty = true;
self._save();
self._notifyViewResize();
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
};
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';
};
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 };
if (this.gameHost) {
this.gameHost.hidden = false;
leaf.appendChild(this.gameHost);
}
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 () {
node.active = pid;
self.rebuild();
self._save();
});
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;
};
WorkbenchLayout.prototype._beginDrag = function (panelId, ev) {
if (this.mode === 'compact') {
ev.preventDefault();
return;
}
this._dragPanel = panelId;
this._hoverGroupCols = null;
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;
// 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();
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 =
'<span class="pw-float-title" draggable="true"></span>' +
'<span class="pw-float-hint">拖标题可停靠</span>' +
'<button type="button" class="pw-btn pw-float-close">关闭</button>';
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._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);
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 () {
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;
};
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();
};
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._notifyViewResize();
};
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);