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
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);
|