(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 = '
' + '
' + '
'; 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.} 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 += ''; } 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);