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.
869 lines
36 KiB
869 lines
36 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;
|
|
this._statsTimer = 0;
|
|
}
|
|
|
|
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();
|
|
self._startLiveStats();
|
|
// 刷新后恢复检视模式(新 Agent 默认 game)
|
|
if (self._inspectOn && self.bridge) {
|
|
self._setInspectCapture(true);
|
|
self.bridge.call('pick.setMode', { mode: 'inspect' }).catch(function () { /* */ });
|
|
} else {
|
|
self._setInspectCapture(false);
|
|
}
|
|
} 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);
|
|
|
|
// 检视捕获层必须在 iframe 之上(挂载时清空了宿主,这里补回)
|
|
var cap = document.createElement('div');
|
|
cap.id = 'pw-pick-capture';
|
|
cap.className = 'pw-pick-capture pw-shell-only';
|
|
cap.hidden = true;
|
|
cap.title = '检视:点击选取节点';
|
|
host.appendChild(cap);
|
|
|
|
// 选中 Gizmo(拖动改坐标 + X/Y 轴箭头)
|
|
var gizmo = document.createElement('div');
|
|
gizmo.id = 'pw-pick-gizmo';
|
|
gizmo.className = 'pw-pick-gizmo pw-shell-only';
|
|
gizmo.hidden = true;
|
|
gizmo.innerHTML =
|
|
'<div class="pw-gizmo-box" data-drag="xy" title="拖动移动节点"></div>' +
|
|
'<div class="pw-gizmo-axis pw-gizmo-x" data-drag="x" title="沿 X 轴拖动"><i></i><b></b></div>' +
|
|
'<div class="pw-gizmo-axis pw-gizmo-y" data-drag="y" title="沿 Y 轴拖动"><i></i><b></b></div>';
|
|
host.appendChild(gizmo);
|
|
this._bindGizmoDrag(gizmo);
|
|
|
|
if (this.layout) this.layout.gameHost = host;
|
|
};
|
|
|
|
/** 刷新并重启游戏(重载 iframe,保留壳布局) */
|
|
PreviewWorkbenchHost.prototype._reloadGame = function () {
|
|
var self = this;
|
|
this._ready = false;
|
|
this._paused = false;
|
|
this._setStatus('正在刷新游戏…', false);
|
|
var pauseBtn = document.getElementById('pw-game-pause');
|
|
if (pauseBtn) {
|
|
pauseBtn.textContent = '暂停';
|
|
pauseBtn.classList.remove('active');
|
|
}
|
|
for (var k in this._subs) {
|
|
if (Object.prototype.hasOwnProperty.call(this._subs, k)) this._subs[k] = false;
|
|
}
|
|
if (this.bridge) {
|
|
try { this.bridge.detach(); } catch (e) { /* */ }
|
|
}
|
|
this._mountGameIframe();
|
|
if (!this._iframe) {
|
|
this._setStatus('刷新失败:无游戏宿主', false);
|
|
return;
|
|
}
|
|
this.bridge = new global.PWBridge();
|
|
this.bridge.attach(this._iframe, { crossOrigin: this._crossOrigin });
|
|
global.__PW_Bridge = this.bridge;
|
|
this._wirePanelsBridge();
|
|
this._bindBridgeEvents();
|
|
this.bridge.on('agent.ready', function (data) {
|
|
self._ready = !!(data && data.ok);
|
|
self._setStatus(self._ready ? '引擎已就绪' : '引擎未就绪', self._ready);
|
|
if (self._ready) {
|
|
self._fillDeviceSelect();
|
|
if (self._workbenchOn) self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
else self.bridge.hibernate().catch(function () { /* */ });
|
|
if (self.layout) self.layout.onEngineReady();
|
|
}
|
|
});
|
|
console.info('[调试工作台] 已刷新游戏 iframe');
|
|
};
|
|
|
|
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);
|
|
if (!data || !data.uuid) self._setPickHighlight(null);
|
|
if (self.bridge) {
|
|
self.bridge.call('pick.getRect').then(function (rect) {
|
|
self._setPickHighlight(rect);
|
|
}).catch(function () { /* */ });
|
|
}
|
|
});
|
|
b.on('pick.rect', function (rect) {
|
|
self._setPickHighlight(rect);
|
|
});
|
|
b.on('perf.sample', function (data) {
|
|
if (self.panels.perf && self.panels.perf.applySample) self.panels.perf.applySample(data);
|
|
});
|
|
b.on('assets.update', function (data) {
|
|
if (self.panels.assets) self.panels.assets.applyData(data);
|
|
if (self.panels.perf && self.panels.perf.applyAssets) self.panels.perf.applyAssets(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);
|
|
}
|
|
}
|
|
// 性能面板可见时启动函数采样剖析
|
|
if (map.perf && this.bridge) {
|
|
this.bridge.call('prof.start').catch(function () { /* */ });
|
|
} else if (this.bridge) {
|
|
this.bridge.call('prof.stop').catch(function () { /* */ });
|
|
}
|
|
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.title = st.rotated ? '竖屏(当前已旋转)' : '旋转';
|
|
rotateBtn.setAttribute('aria-label', st.rotated ? '竖屏' : '旋转');
|
|
}
|
|
}).catch(function () { /* */ });
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._bindToolbar = function () {
|
|
var self = this;
|
|
document.getElementById('pw-mode-game').addEventListener('click', function () {
|
|
self._inspectOn = false;
|
|
self._setInspectCapture(false);
|
|
self._setPickHighlight(null);
|
|
self.bridge.call('pick.setMode', { mode: 'game' }).catch(function () { /* */ });
|
|
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._setInspectCapture(true);
|
|
self.bridge.call('pick.setMode', { mode: 'inspect' }).catch(function () { /* */ });
|
|
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.title = rotated ? '竖屏(当前已旋转)' : '旋转';
|
|
rotateBtn.setAttribute('aria-label', rotated ? '竖屏' : '旋转');
|
|
});
|
|
});
|
|
}
|
|
var capBtn = document.getElementById('pw-device-capsule-btn');
|
|
if (capBtn) {
|
|
var syncCap = function (on) {
|
|
capBtn.classList.toggle('active', !!on);
|
|
capBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
};
|
|
capBtn.addEventListener('click', function () {
|
|
if (!self.bridge) return;
|
|
self.bridge.call('device.getState').then(function (st) {
|
|
var next = !(st && st.capsule);
|
|
return self.bridge.call('device.setCapsule', { on: next }).then(function (on) {
|
|
syncCap(on);
|
|
});
|
|
}).catch(function () { /* */ });
|
|
});
|
|
if (self.bridge) {
|
|
self.bridge.call('device.getState').then(function (st) {
|
|
syncCap(st && st.capsule);
|
|
}).catch(function () { /* */ });
|
|
}
|
|
}
|
|
var capStyle = document.getElementById('pw-capsule-style');
|
|
if (capStyle && self.bridge) {
|
|
capStyle.addEventListener('change', function () {
|
|
self.bridge.call('device.setCapsuleStyle', { style: capStyle.value }).catch(function () { /* */ });
|
|
});
|
|
self.bridge.call('device.getCapsuleStyle').then(function (s) {
|
|
if (s) capStyle.value = s;
|
|
}).catch(function () { /* */ });
|
|
}
|
|
|
|
var refreshBtn = document.getElementById('pw-game-refresh');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', function () {
|
|
self._reloadGame();
|
|
});
|
|
}
|
|
|
|
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._stopLiveStats(true);
|
|
self._setStatus('纯净测帧中', true);
|
|
} else {
|
|
self.bridge.wake().then(function () {
|
|
self._syncSubscriptions(self.layout.getVisiblePanels());
|
|
self._setStatus('引擎已就绪', true);
|
|
self._startLiveStats();
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/** 右上角实时 FPS / 总内存(按需拉取,纯净测帧时停) */
|
|
PreviewWorkbenchHost.prototype._startLiveStats = function () {
|
|
var self = this;
|
|
this._stopLiveStats(false);
|
|
if (this._pure || !this.bridge) return;
|
|
this._statsTimer = setInterval(function () {
|
|
if (self._pure || !self.bridge) return;
|
|
self.bridge.call('stats.get').then(function (s) {
|
|
self._applyLiveStats(s);
|
|
}).catch(function () { /* */ });
|
|
}, 500);
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._stopLiveStats = function (blank) {
|
|
if (this._statsTimer) {
|
|
clearInterval(this._statsTimer);
|
|
this._statsTimer = 0;
|
|
}
|
|
if (blank) this._applyLiveStats(null);
|
|
};
|
|
|
|
PreviewWorkbenchHost.prototype._applyLiveStats = function (s) {
|
|
var fpsEl = document.getElementById('pw-stat-fps');
|
|
var memEl = document.getElementById('pw-stat-mem');
|
|
if (!fpsEl || !memEl) return;
|
|
var levels = ['ok', 'good', 'warn', 'hot', 'bad', 'muted'];
|
|
var clear = function (el) {
|
|
for (var i = 0; i < levels.length; i++) el.classList.remove(levels[i]);
|
|
};
|
|
if (!s) {
|
|
fpsEl.textContent = 'FPS --';
|
|
memEl.textContent = '内存 --';
|
|
clear(fpsEl);
|
|
clear(memEl);
|
|
fpsEl.classList.add('muted');
|
|
memEl.classList.add('muted');
|
|
return;
|
|
}
|
|
var fps = Math.round(s.fps || 0);
|
|
var mem = Math.round((s.totalMemory || 0) * 10) / 10;
|
|
fpsEl.textContent = 'FPS ' + fps;
|
|
memEl.textContent = '内存 ' + mem + ' MB';
|
|
|
|
clear(fpsEl);
|
|
if (fps <= 0) fpsEl.classList.add('muted');
|
|
else if (fps >= 55) fpsEl.classList.add('ok');
|
|
else if (fps >= 40) fpsEl.classList.add('good');
|
|
else if (fps >= 25) fpsEl.classList.add('warn');
|
|
else if (fps >= 15) fpsEl.classList.add('hot');
|
|
else fpsEl.classList.add('bad');
|
|
|
|
clear(memEl);
|
|
if (mem <= 0) memEl.classList.add('muted');
|
|
else if (mem < 150) memEl.classList.add('ok');
|
|
else if (mem < 300) memEl.classList.add('good');
|
|
else if (mem < 500) memEl.classList.add('warn');
|
|
else memEl.classList.add('hot');
|
|
};
|
|
|
|
/**
|
|
* 检视捕获层:盖在游戏 iframe 上,把点击转成 pick.at(不依赖游戏内 DOM 事件)。
|
|
* @param {boolean} on
|
|
*/
|
|
PreviewWorkbenchHost.prototype._setInspectCapture = function (on) {
|
|
var self = this;
|
|
var layer = document.getElementById('pw-pick-capture');
|
|
var root = document.getElementById('pw-root');
|
|
if (root) root.classList.toggle('pw-inspect', !!on);
|
|
document.documentElement.classList.toggle('pw-inspect', !!on);
|
|
if (!layer) return;
|
|
layer.hidden = !on;
|
|
if (this._pickCaptureBound) {
|
|
layer.removeEventListener('pointerdown', this._onPickCapture, true);
|
|
layer.removeEventListener('mousedown', this._onPickCapture, true);
|
|
this._pickCaptureBound = false;
|
|
}
|
|
if (!on) return;
|
|
if (!this._onPickCapture) {
|
|
this._onPickCapture = function (ev) {
|
|
if (!self._inspectOn) return;
|
|
if (ev.preventDefault) ev.preventDefault();
|
|
if (ev.stopPropagation) ev.stopPropagation();
|
|
if (ev.stopImmediatePropagation) ev.stopImmediatePropagation();
|
|
var gameX = ev.clientX;
|
|
var gameY = ev.clientY;
|
|
var iframe = self._iframe;
|
|
if (iframe) {
|
|
try {
|
|
var r = iframe.getBoundingClientRect();
|
|
gameX = ev.clientX - r.left;
|
|
gameY = ev.clientY - r.top;
|
|
// 同域时用游戏内 canvas 矩形再换算一层(兼容设备壳偏移)
|
|
var cwin = iframe.contentWindow;
|
|
var cdoc = cwin && cwin.document;
|
|
var cel = cdoc && (cdoc.getElementById('GameCanvas') || (cwin.cc && cwin.cc.game && cwin.cc.game.canvas));
|
|
if (cel && cel.getBoundingClientRect) {
|
|
// gameX/Y 已是 iframe 视口坐标;Agent 会减 canvas.rect
|
|
// 这里保持 viewport 坐标即可
|
|
}
|
|
} catch (e) { /* */ }
|
|
}
|
|
if (!self.bridge) return;
|
|
self.bridge.call('pick.at', {
|
|
clientX: gameX,
|
|
clientY: gameY,
|
|
space: 'viewport',
|
|
}).then(function (res) {
|
|
var name = res && res.debug && res.debug.best ? res.debug.best : '';
|
|
if (res && res.uuid) {
|
|
var label = name || res.uuid.slice(0, 8);
|
|
if (res.cycle && res.cycle.total > 1) {
|
|
label += ' (' + (res.cycle.index + 1) + '/' + res.cycle.total + ')';
|
|
}
|
|
self._setStatus('已选中 ' + label, true);
|
|
if (self.panels.inspector) self.panels.inspector.loadUuid(res.uuid);
|
|
if (self.panels.hierarchy) self.panels.hierarchy.setSelectedUuid(res.uuid);
|
|
self._setPickHighlight(res.rect || null);
|
|
if (!res.rect && self.bridge) {
|
|
self.bridge.call('pick.getRect').then(function (rect) {
|
|
self._setPickHighlight(rect);
|
|
}).catch(function () { /* */ });
|
|
}
|
|
} else {
|
|
self._setStatus('未命中 (' + Math.round(gameX) + ',' + Math.round(gameY) + ')', false);
|
|
self._setPickHighlight(null);
|
|
}
|
|
}).catch(function (err) {
|
|
self._setStatus('选取失败', false);
|
|
console.warn('[检视] pick.at', err);
|
|
});
|
|
};
|
|
}
|
|
layer.addEventListener('pointerdown', this._onPickCapture, true);
|
|
layer.addEventListener('mousedown', this._onPickCapture, true);
|
|
this._pickCaptureBound = true;
|
|
};
|
|
|
|
/**
|
|
* 选中高亮:
|
|
* - 游戏模式:只保留游戏内绿框,不显示坐标轴、禁止拖动
|
|
* - 检视模式:绿框 + 中心坐标轴;点击循环选中,拖动才移动
|
|
*/
|
|
PreviewWorkbenchHost.prototype._setPickHighlight = function (rect) {
|
|
var gizmo = document.getElementById('pw-pick-gizmo');
|
|
if (!gizmo) return;
|
|
|
|
// 非检视:隐藏 Gizmo(绿框由游戏内 overlay 绘制)
|
|
if (!this._inspectOn) {
|
|
gizmo.hidden = true;
|
|
this._gizmoState = null;
|
|
return;
|
|
}
|
|
if (!rect || !(rect.width > 0) || !(rect.height > 0)) {
|
|
gizmo.hidden = true;
|
|
this._gizmoState = null;
|
|
return;
|
|
}
|
|
var host = document.getElementById('pw-game-host');
|
|
var ox = 0;
|
|
var oy = 0;
|
|
if (host && this._iframe) {
|
|
var hr = host.getBoundingClientRect();
|
|
var ir = this._iframe.getBoundingClientRect();
|
|
ox = ir.left - hr.left;
|
|
oy = ir.top - hr.top;
|
|
}
|
|
gizmo.hidden = false;
|
|
gizmo.style.left = (ox + (rect.left || 0)) + 'px';
|
|
gizmo.style.top = (oy + (rect.top || 0)) + 'px';
|
|
gizmo.style.width = Math.max(4, rect.width || 0) + 'px';
|
|
gizmo.style.height = Math.max(4, rect.height || 0) + 'px';
|
|
this._gizmoState = {
|
|
uuid: rect.uuid || '',
|
|
pos: rect.pos || { x: 0, y: 0, z: 0 },
|
|
scale: rect.scale || { x: 1, y: 1 },
|
|
ox: ox,
|
|
oy: oy,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Gizmo:位移阈值内视为点击(循环选中),超过阈值才拖动改坐标。
|
|
* 轴从节点中心画出。
|
|
*/
|
|
PreviewWorkbenchHost.prototype._bindGizmoDrag = function (gizmo) {
|
|
var self = this;
|
|
var pending = null;
|
|
var THRESHOLD = 4;
|
|
|
|
var onDown = function (ev) {
|
|
if (ev.button != null && ev.button !== 0) return;
|
|
if (!self._inspectOn) return;
|
|
var axis = ev.currentTarget.getAttribute('data-drag') || 'xy';
|
|
var st = self._gizmoState;
|
|
if (!st || !st.uuid) return;
|
|
// 先不拦截,留给点击;真拖动再 prevent
|
|
pending = {
|
|
axis: axis,
|
|
x0: ev.clientX,
|
|
y0: ev.clientY,
|
|
px: st.pos.x || 0,
|
|
py: st.pos.y || 0,
|
|
sx: st.scale.x || 1,
|
|
sy: st.scale.y || 1,
|
|
uuid: st.uuid,
|
|
left0: parseFloat(gizmo.style.left) || 0,
|
|
top0: parseFloat(gizmo.style.top) || 0,
|
|
dragging: false,
|
|
};
|
|
document.addEventListener('pointermove', onMove);
|
|
document.addEventListener('pointerup', onUp);
|
|
};
|
|
|
|
var onMove = function (ev) {
|
|
if (!pending) return;
|
|
var dx = ev.clientX - pending.x0;
|
|
var dy = ev.clientY - pending.y0;
|
|
if (!pending.dragging) {
|
|
if (Math.abs(dx) < THRESHOLD && Math.abs(dy) < THRESHOLD) return;
|
|
pending.dragging = true;
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
gizmo.classList.add('dragging');
|
|
}
|
|
ev.preventDefault();
|
|
var nx = pending.px;
|
|
var ny = pending.py;
|
|
if (pending.axis !== 'y') nx = pending.px + dx * pending.sx;
|
|
if (pending.axis !== 'x') ny = pending.py - dy * pending.sy;
|
|
nx = Math.round(nx * 100) / 100;
|
|
ny = Math.round(ny * 100) / 100;
|
|
if (self.bridge) {
|
|
if (pending.axis !== 'y') {
|
|
self.bridge.call('node.set', { uuid: pending.uuid, fid: 'px', value: nx });
|
|
}
|
|
if (pending.axis !== 'x') {
|
|
self.bridge.call('node.set', { uuid: pending.uuid, fid: 'py', value: ny });
|
|
}
|
|
}
|
|
if (pending.axis !== 'y') gizmo.style.left = (pending.left0 + dx) + 'px';
|
|
if (pending.axis !== 'x') gizmo.style.top = (pending.top0 + dy) + 'px';
|
|
if (self._gizmoState) {
|
|
self._gizmoState.pos = {
|
|
x: nx,
|
|
y: ny,
|
|
z: (self._gizmoState.pos && self._gizmoState.pos.z) || 0,
|
|
};
|
|
}
|
|
};
|
|
|
|
var onUp = function (ev) {
|
|
document.removeEventListener('pointermove', onMove);
|
|
document.removeEventListener('pointerup', onUp);
|
|
if (!pending) return;
|
|
var st = pending;
|
|
pending = null;
|
|
gizmo.classList.remove('dragging');
|
|
|
|
if (!st.dragging) {
|
|
// 视为点击 → 循环选中重叠节点
|
|
var iframe = self._iframe;
|
|
var gameX = ev.clientX;
|
|
var gameY = ev.clientY;
|
|
if (iframe) {
|
|
var r = iframe.getBoundingClientRect();
|
|
gameX = ev.clientX - r.left;
|
|
gameY = ev.clientY - r.top;
|
|
}
|
|
if (self.bridge) {
|
|
self.bridge.call('pick.at', { clientX: gameX, clientY: gameY, space: 'viewport' })
|
|
.then(function (res) {
|
|
if (res && res.uuid) {
|
|
if (self.panels.inspector) self.panels.inspector.loadUuid(res.uuid);
|
|
if (self.panels.hierarchy) self.panels.hierarchy.setSelectedUuid(res.uuid);
|
|
}
|
|
return self.bridge.call('pick.getRect');
|
|
})
|
|
.then(function (rect) {
|
|
self._setPickHighlight(rect);
|
|
})
|
|
.catch(function () { /* */ });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 真正拖动结束 → 刷新框与属性
|
|
if (self.bridge) {
|
|
self.bridge.call('pick.getRect').then(function (r) {
|
|
self._setPickHighlight(r);
|
|
}).catch(function () { /* */ });
|
|
if (self.panels.inspector) self.panels.inspector.loadUuid(st.uuid);
|
|
}
|
|
};
|
|
|
|
var handles = gizmo.querySelectorAll('[data-drag]');
|
|
for (var i = 0; i < handles.length; i++) {
|
|
handles[i].addEventListener('pointerdown', onDown);
|
|
handles[i].addEventListener('mousedown', onDown);
|
|
}
|
|
};
|
|
|
|
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);
|
|
|