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.

426 lines
16 KiB

3 days ago
(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: '控制台',
};
/**
* 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;
/** @type {function(Object.<string,boolean>):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.<string,boolean>} */
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 () {
this._syncPanelButtons();
if (typeof this.onVisibilityChange === 'function') {
this.onVisibilityChange(this.getVisiblePanels());
}
};
/** 面板开关按钮高亮:打开 active,关闭灭(与 FPS 按钮一致) */
WorkbenchLayout.prototype._syncPanelButtons = function () {
var map = this.getVisiblePanels();
var buttons = this.root.querySelectorAll('.pw-panel-btn');
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var pid = btn.getAttribute('data-panel');
if (!pid) continue;
var on = !!map[pid];
btn.classList.toggle('active', on);
btn.setAttribute('aria-pressed', on ? 'true' : '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') {
var next = this._presetTree('analyze');
// 工作台关闭时只更新备份,避免与「仅游戏」树冲突导致布局抖动
if (this._workbenchOff) {
this._treeBeforeOff = next;
return;
}
this._tree = next;
this.rebuild();
this._save();
}
};
/** 设计分辨率是否竖屏(高 ≥ 宽) */
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._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);