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.
439 lines
18 KiB
439 lines
18 KiB
(function (global) {
|
|
'use strict';
|
|
|
|
/**
|
|
* 工作台壳:布局 + 面板 + Bridge;游戏在 iframe(?pw=game)内由 Agent 驱动。
|
|
*/
|
|
function PreviewWorkbenchHost() {
|
|
this.layout = null;
|
|
this.bridge = null;
|
|
this.panels = {};
|
|
this._ready = false;
|
|
this._paused = false;
|
|
this._fpsOn = false;
|
|
this._pure = false;
|
|
this._workbenchOn = true;
|
|
this._subs = {};
|
|
this._iframe = null;
|
|
this._crossOrigin = false;
|
|
}
|
|
|
|
PreviewWorkbenchHost.prototype.boot = function () {
|
|
var root = document.getElementById('pw-root');
|
|
if (!root) {
|
|
console.warn('[调试工作台] 缺少 #pw-root');
|
|
return;
|
|
}
|
|
|
|
// 壳页不再加载引擎;仅挂游戏 iframe
|
|
this._mountGameIframe();
|
|
|
|
this.bridge = new global.PWBridge();
|
|
this.bridge.attach(this._iframe, { crossOrigin: this._crossOrigin });
|
|
global.__PW_Bridge = this.bridge;
|
|
|
|
this.layout = new global.PWLayout(root);
|
|
this.panels.console = new global.PWConsolePanel();
|
|
this.panels.hierarchy = new global.PWHierarchyPanel();
|
|
this.panels.inspector = new global.PWInspectorPanel();
|
|
this.panels.assets = new global.PWAssetsPanel();
|
|
this.panels.perf = new global.PWPerfPanel();
|
|
|
|
this.panels.console.setBridge(this.bridge);
|
|
this.panels.hierarchy.setBridge(this.bridge);
|
|
this.panels.inspector.setBridge(this.bridge);
|
|
this.panels.assets.setBridge(this.bridge);
|
|
this.panels.perf.setBridge(this.bridge);
|
|
|
|
this.layout.registerPanel('console', this.panels.console.mount(), '控制台');
|
|
this.layout.registerPanel('hierarchy', this.panels.hierarchy.mount(), '层级');
|
|
this.layout.registerPanel('inspector', this.panels.inspector.mount(), '属性');
|
|
this.layout.registerPanel('assets', this.panels.assets.mount(), '资源');
|
|
this.layout.registerPanel('perf', this.panels.perf.mount(), '性能');
|
|
this.layout.init();
|
|
|
|
var self = this;
|
|
this.layout.onVisibilityChange = function (visibleMap) {
|
|
self._syncSubscriptions(visibleMap);
|
|
};
|
|
|
|
this._bindToolbar();
|
|
this._bindBridgeEvents();
|
|
this._setStatus('引擎等待中…', false);
|
|
|
|
this.bridge.on('agent.ready', function (data) {
|
|
self._ready = !!(data && data.ok);
|
|
self._setStatus(self._ready ? '引擎已就绪' : '引擎未就绪', self._ready);
|
|
if (self._ready) {
|
|
self._fillDeviceSelect();
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
if (self.layout) self.layout.onEngineReady();
|
|
} else if (self._crossOrigin && !self._fallbackTried) {
|
|
self._fallbackTried = true;
|
|
global.PWBridge.markCrossOriginFailed();
|
|
self._remountSameOrigin();
|
|
}
|
|
});
|
|
|
|
// 跨域超时降级
|
|
setTimeout(function () {
|
|
if (!self._ready && self._crossOrigin && !self._fallbackTried) {
|
|
self._fallbackTried = true;
|
|
global.PWBridge.markCrossOriginFailed();
|
|
self._setStatus('跨域失败,改用同域…', false);
|
|
self._remountSameOrigin();
|
|
}
|
|
}, 5000);
|
|
|
|
global.__PreviewWorkbench = {
|
|
mark: function (label) {
|
|
if (self.bridge) self.bridge.call('perf.mark', { text: label });
|
|
},
|
|
bridge: self.bridge,
|
|
isReady: function () { return self._ready; },
|
|
isPure: function () { return self._pure; },
|
|
isWorkbenchOn: function () { return self._workbenchOn; },
|
|
};
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._mountGameIframe = function () {
|
|
// 防御:壳脚本若被嵌进 iframe,禁止再套一层
|
|
if (global.parent && global.parent !== global) {
|
|
console.warn('[调试工作台] 壳页处于 iframe 内,跳过再次嵌套');
|
|
return;
|
|
}
|
|
var host = document.getElementById('pw-game-host');
|
|
if (!host) return;
|
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
host.hidden = false;
|
|
host.classList.add('pw-game-iframe-host');
|
|
host.style.pointerEvents = 'auto';
|
|
|
|
// 默认同域,避免跨域预览资源/输入异常
|
|
var info = global.PWBridge.buildGameUrl(true);
|
|
this._crossOrigin = false;
|
|
var iframe = document.createElement('iframe');
|
|
iframe.id = 'pw-game-iframe';
|
|
iframe.className = 'pw-game-iframe';
|
|
iframe.setAttribute('allow', 'autoplay; fullscreen');
|
|
iframe.style.pointerEvents = 'auto';
|
|
iframe.src = info.url;
|
|
host.appendChild(iframe);
|
|
this._iframe = iframe;
|
|
console.info('[调试工作台] 已挂载游戏 iframe', info.url);
|
|
|
|
if (this.layout) this.layout.gameHost = host;
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._remountSameOrigin = function () {
|
|
if (this.bridge) this.bridge.detach();
|
|
var info = global.PWBridge.buildGameUrl(true);
|
|
this._crossOrigin = false;
|
|
var host = document.getElementById('pw-game-host');
|
|
if (!host) return;
|
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
var iframe = document.createElement('iframe');
|
|
iframe.id = 'pw-game-iframe';
|
|
iframe.className = 'pw-game-iframe';
|
|
iframe.style.pointerEvents = 'auto';
|
|
iframe.src = info.url;
|
|
host.appendChild(iframe);
|
|
this._iframe = iframe;
|
|
this.bridge = new global.PWBridge();
|
|
this.bridge.attach(iframe, { crossOrigin: false });
|
|
global.__PW_Bridge = this.bridge;
|
|
this._wirePanelsBridge();
|
|
this._bindBridgeEvents();
|
|
var self = this;
|
|
this.bridge.on('agent.ready', function (data) {
|
|
self._ready = !!(data && data.ok);
|
|
self._setStatus(self._ready ? '引擎已就绪(同域)' : '引擎未就绪', self._ready);
|
|
if (self._ready) {
|
|
self._fillDeviceSelect();
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
}
|
|
});
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._wirePanelsBridge = function () {
|
|
var b = this.bridge;
|
|
if (this.panels.console) this.panels.console.setBridge(b);
|
|
if (this.panels.hierarchy) this.panels.hierarchy.setBridge(b);
|
|
if (this.panels.inspector) this.panels.inspector.setBridge(b);
|
|
if (this.panels.assets) this.panels.assets.setBridge(b);
|
|
if (this.panels.perf) this.panels.perf.setBridge(b);
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._bindBridgeEvents = function () {
|
|
var self = this;
|
|
var b = this.bridge;
|
|
if (!b) return;
|
|
b.on('hier.diff', function (data) {
|
|
if (self.panels.hierarchy) self.panels.hierarchy.applyTree(data);
|
|
});
|
|
b.on('pick.selected', function (data) {
|
|
if (self.panels.hierarchy) self.panels.hierarchy.setSelectedUuid(data && data.uuid);
|
|
if (self.panels.inspector) self.panels.inspector.loadUuid(data && data.uuid);
|
|
});
|
|
b.on('perf.sample', function (data) {
|
|
if (self.panels.perf) self.panels.perf.applySample(data);
|
|
});
|
|
b.on('assets.update', function (data) {
|
|
if (self.panels.assets) self.panels.assets.applyData(data);
|
|
});
|
|
b.on('console.batch', function (data) {
|
|
if (self.panels.console) self.panels.console.applyBatch(data && data.items);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 按可见面板订阅 Agent 通道。
|
|
* @param {Object.<string,boolean>} visibleMap
|
|
*/
|
|
PreviewWorkbenchHost.prototype._syncSubscriptions = function (visibleMap) {
|
|
if (!this.bridge || this._pure) return;
|
|
var map = visibleMap || {};
|
|
var channelOf = {
|
|
perf: 'perf',
|
|
hierarchy: 'hierarchy',
|
|
inspector: 'inspector',
|
|
assets: 'assets',
|
|
console: 'console',
|
|
};
|
|
var want = {};
|
|
for (var pid in channelOf) {
|
|
if (!Object.prototype.hasOwnProperty.call(channelOf, pid)) continue;
|
|
if (map[pid]) want[channelOf[pid]] = true;
|
|
}
|
|
if (map.perf) want.markers = true;
|
|
// 检视模式由工具栏单独订阅 pick
|
|
if (this._inspectOn) want.pick = true;
|
|
|
|
var all = ['perf', 'hierarchy', 'inspector', 'assets', 'console', 'markers', 'pick'];
|
|
for (var i = 0; i < all.length; i++) {
|
|
var ch = all[i];
|
|
var on = !!want[ch];
|
|
var was = !!this._subs[ch];
|
|
if (on && !was) {
|
|
this._subs[ch] = true;
|
|
this.bridge.subscribe(ch);
|
|
} else if (!on && was) {
|
|
this._subs[ch] = false;
|
|
this.bridge.unsubscribe(ch);
|
|
}
|
|
}
|
|
var self = this;
|
|
if (map.hierarchy) {
|
|
this.bridge.call('hier.getTree').then(function (tree) {
|
|
if (self.panels.hierarchy) self.panels.hierarchy.applyTree(tree);
|
|
}).catch(function () { /* */ });
|
|
}
|
|
if (map.assets) {
|
|
this.bridge.call('assets.list').then(function (data) {
|
|
if (self.panels.assets) self.panels.assets.applyData(data);
|
|
}).catch(function () { /* */ });
|
|
}
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._fillDeviceSelect = function () {
|
|
var self = this;
|
|
var sel = document.getElementById('pw-device-select');
|
|
if (!sel || !this.bridge) return;
|
|
this.bridge.call('device.list').then(function (list) {
|
|
if (!list) return;
|
|
var html = '';
|
|
for (var i = 0; i < list.length; i++) {
|
|
var d = list[i];
|
|
html += '<option value="' + d.id + '">' + d.name + (d.w ? ' (' + d.w + '×' + d.h + ')' : '') + '</option>';
|
|
}
|
|
sel.innerHTML = html;
|
|
return self.bridge.call('device.getState');
|
|
}).then(function (st) {
|
|
if (st && sel) sel.value = st.id;
|
|
var rotateBtn = document.getElementById('pw-device-rotate');
|
|
if (rotateBtn && st) {
|
|
rotateBtn.classList.toggle('active', !!st.rotated);
|
|
rotateBtn.textContent = st.rotated ? '竖屏' : '旋转';
|
|
}
|
|
}).catch(function () { /* */ });
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._bindToolbar = function () {
|
|
var self = this;
|
|
document.getElementById('pw-mode-game').addEventListener('click', function () {
|
|
self._inspectOn = false;
|
|
self.bridge.call('pick.setMode', { mode: 'game' });
|
|
document.getElementById('pw-mode-game').classList.add('active');
|
|
document.getElementById('pw-mode-inspect').classList.remove('active');
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
});
|
|
document.getElementById('pw-mode-inspect').addEventListener('click', function () {
|
|
self._inspectOn = true;
|
|
self.bridge.call('pick.setMode', { mode: 'inspect' });
|
|
document.getElementById('pw-mode-inspect').classList.add('active');
|
|
document.getElementById('pw-mode-game').classList.remove('active');
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
});
|
|
|
|
var deviceSel = document.getElementById('pw-device-select');
|
|
if (deviceSel) {
|
|
deviceSel.addEventListener('change', function () {
|
|
self.bridge.call('device.set', { id: deviceSel.value });
|
|
});
|
|
}
|
|
var rotateBtn = document.getElementById('pw-device-rotate');
|
|
if (rotateBtn) {
|
|
rotateBtn.addEventListener('click', function () {
|
|
self.bridge.call('device.rotate').then(function (rotated) {
|
|
rotateBtn.classList.toggle('active', !!rotated);
|
|
rotateBtn.textContent = rotated ? '竖屏' : '旋转';
|
|
});
|
|
});
|
|
}
|
|
|
|
var pauseBtn = document.getElementById('pw-game-pause');
|
|
if (pauseBtn) {
|
|
pauseBtn.addEventListener('click', function () {
|
|
self._paused = !self._paused;
|
|
self.bridge.call(self._paused ? 'game.pause' : 'game.resume');
|
|
pauseBtn.textContent = self._paused ? '继续' : '暂停';
|
|
pauseBtn.classList.toggle('active', self._paused);
|
|
});
|
|
}
|
|
|
|
var bindStep = function (btn) {
|
|
if (!btn) return;
|
|
btn.addEventListener('click', function () {
|
|
if (!self._paused) {
|
|
self._paused = true;
|
|
self.bridge.call('game.pause');
|
|
if (pauseBtn) {
|
|
pauseBtn.textContent = '继续';
|
|
pauseBtn.classList.add('active');
|
|
}
|
|
}
|
|
self.bridge.call('game.step');
|
|
});
|
|
};
|
|
bindStep(document.getElementById('pw-game-step'));
|
|
bindStep(document.getElementById('pw-game-step-compact'));
|
|
|
|
this._bindFpsButton();
|
|
this._bindPureButton();
|
|
this._bindWorkbenchToggle();
|
|
};
|
|
|
|
/** 右上角:一键开启/关闭调试工作台 */
|
|
PreviewWorkbenchHost.prototype._bindWorkbenchToggle = function () {
|
|
var self = this;
|
|
var btn = document.getElementById('pw-workbench-toggle');
|
|
if (!btn) return;
|
|
var apply = function (on, persist) {
|
|
self._workbenchOn = !!on;
|
|
btn.classList.toggle('active', self._workbenchOn);
|
|
btn.setAttribute('aria-pressed', self._workbenchOn ? 'true' : 'false');
|
|
btn.textContent = self._workbenchOn ? '工作台' : '工作台(关)';
|
|
if (self.layout) self.layout.setWorkbenchEnabled(self._workbenchOn);
|
|
if (!self.bridge) return;
|
|
if (!self._workbenchOn) {
|
|
self._inspectOn = false;
|
|
self.bridge.call('pick.setMode', { mode: 'game' }).catch(function () { /* */ });
|
|
self.bridge.hibernate().catch(function () { /* */ });
|
|
for (var k in self._subs) {
|
|
if (Object.prototype.hasOwnProperty.call(self._subs, k)) self._subs[k] = false;
|
|
}
|
|
self._setStatus('工作台已关闭', true);
|
|
} else {
|
|
self.bridge.wake().then(function () {
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
self._setStatus(self._ready ? '引擎已就绪' : '引擎等待中…', self._ready);
|
|
}).catch(function () { /* */ });
|
|
}
|
|
if (persist !== false) {
|
|
try {
|
|
localStorage.setItem('preview-workbench-enabled', self._workbenchOn ? '1' : '0');
|
|
} catch (e) { /* */ }
|
|
}
|
|
};
|
|
btn.addEventListener('click', function () {
|
|
apply(!self._workbenchOn, true);
|
|
});
|
|
var saved = null;
|
|
try { saved = localStorage.getItem('preview-workbench-enabled'); } catch (e2) { /* */ }
|
|
// 新用户默认关闭;须在 layout.init / 悬浮窗还原完成后再关,禁止启动期就置 off
|
|
if (saved === '1') {
|
|
apply(true, false);
|
|
} else {
|
|
setTimeout(function () { apply(false, saved == null); }, 0);
|
|
}
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._bindFpsButton = function () {
|
|
var self = this;
|
|
var main = document.getElementById('pw-show-fps');
|
|
var compact = document.getElementById('pw-show-fps-compact');
|
|
var syncUi = function (on) {
|
|
if (main) {
|
|
main.classList.toggle('active', on);
|
|
main.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
}
|
|
if (compact) {
|
|
compact.classList.toggle('active', on);
|
|
compact.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
}
|
|
};
|
|
var apply = function (on) {
|
|
self._fpsOn = !!on;
|
|
syncUi(self._fpsOn);
|
|
if (self.bridge) self.bridge.call('game.fps', { on: self._fpsOn });
|
|
};
|
|
var toggle = function () { apply(!self._fpsOn); };
|
|
if (main) main.addEventListener('click', toggle);
|
|
if (compact) compact.addEventListener('click', toggle);
|
|
syncUi(false);
|
|
setTimeout(function () { apply(false); }, 600);
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._bindPureButton = function () {
|
|
var self = this;
|
|
var btn = document.getElementById('pw-pure-measure');
|
|
if (!btn) return;
|
|
btn.addEventListener('click', function () {
|
|
self._pure = !self._pure;
|
|
btn.classList.toggle('active', self._pure);
|
|
btn.setAttribute('aria-pressed', self._pure ? 'true' : 'false');
|
|
if (self._pure) {
|
|
self.bridge.hibernate();
|
|
for (var k in self._subs) {
|
|
if (Object.prototype.hasOwnProperty.call(self._subs, k)) self._subs[k] = false;
|
|
}
|
|
self._setStatus('纯净测帧中', true);
|
|
} else {
|
|
self.bridge.wake().then(function () {
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
self._setStatus('引擎已就绪', true);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._setStatus = function (text, ok) {
|
|
var el = document.getElementById('pw-engine-status');
|
|
if (!el) return;
|
|
el.textContent = text;
|
|
el.classList.toggle('ok', !!ok);
|
|
el.classList.toggle('err', ok === false);
|
|
};
|
|
|
|
function start() {
|
|
if (global.__PW_MODE === 'game') return;
|
|
var host = new PreviewWorkbenchHost();
|
|
host.boot();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', start);
|
|
} else {
|
|
start();
|
|
}
|
|
})(window);
|
|
|