diff --git a/preview-template/index.ejs b/preview-template/index.ejs
index 74b4612..aa4014b 100644
--- a/preview-template/index.ejs
+++ b/preview-template/index.ejs
@@ -54,6 +54,7 @@
旋转
+
刷新
FPS
暂停
按帧执行
diff --git a/preview-template/workbench/css/workbench.css b/preview-template/workbench/css/workbench.css
index 6a3e467..33e9dce 100644
--- a/preview-template/workbench/css/workbench.css
+++ b/preview-template/workbench/css/workbench.css
@@ -88,22 +88,27 @@ html[data-pw='shell'] .pw-shell-dummy-canvas,
display: none !important;
}
-.pw-root.pw-workbench-off .pw-toolbar-center > :not(#pw-device-select):not(#pw-device-rotate) {
+.pw-root.pw-workbench-off .pw-toolbar-center > :not(#pw-device-select):not(#pw-device-rotate):not(#pw-game-refresh) {
display: none !important;
}
.pw-root.pw-workbench-off .pw-toolbar {
- justify-content: space-between;
+ position: relative;
+ justify-content: center;
}
.pw-root.pw-workbench-off .pw-toolbar-center {
- flex: 1 1 auto;
- justify-content: flex-start;
+ flex: 0 0 auto;
+ justify-content: center;
}
.pw-root.pw-workbench-off .pw-toolbar-right {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
flex: 0 0 auto;
- margin-left: auto;
+ margin: 0;
}
.pw-root.pw-workbench-off #pw-workspace {
@@ -715,6 +720,84 @@ body {
}
/* —— 控制台(对齐 Chrome DevTools Console) —— */
+.pw-console-head {
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.pw-console-tools {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+ min-width: 0;
+}
+
+.pw-console-levels {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.pw-console-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ height: 22px;
+ padding: 0 8px;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background: transparent;
+ color: #c7c7c7;
+ font-size: 11px;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.pw-console-chip:hover {
+ background: #3c4043;
+ color: #e8eaed;
+}
+
+.pw-console-chip.active {
+ background: #3c4043;
+ color: #fff;
+ border-color: #5f6368;
+}
+
+.pw-console-chip[data-level="error"].active {
+ background: #4b1113;
+ border-color: #8a1a1f;
+ color: #ffc7c7;
+}
+
+.pw-console-chip[data-level="warn"].active {
+ background: #332b00;
+ border-color: #634b00;
+ color: #fdd663;
+}
+
+.pw-console-chip-n {
+ min-width: 14px;
+ padding: 0 4px;
+ border-radius: 7px;
+ background: #202124;
+ color: #9aa0a6;
+ font-size: 10px;
+ line-height: 14px;
+ text-align: center;
+}
+
+.pw-console-chip.active .pw-console-chip-n {
+ background: rgba(0, 0, 0, 0.35);
+ color: inherit;
+}
+
+.pw-console-filter {
+ width: 120px;
+ max-width: 40vw;
+}
+
.pw-console-list,
.pw-console-list * {
font-family: "Consolas", "Courier New", Menlo, Monaco, monospace !important;
@@ -727,6 +810,19 @@ body {
line-height: 1.4 !important;
color: #e8eaed;
padding: 0;
+ user-select: text;
+ -webkit-user-select: text;
+}
+
+.pw-console-list .pw-console-msg,
+.pw-console-list .pw-console-stack,
+.pw-console-list .pw-v-fmt,
+.pw-console-list .pw-v-text,
+.pw-console-list .pw-v-string,
+.pw-console-list .pw-v-error {
+ user-select: text;
+ -webkit-user-select: text;
+ cursor: text;
}
.pw-console-empty {
@@ -804,11 +900,47 @@ body {
color: inherit;
}
+.pw-console-src {
+ flex: 0 0 auto;
+ max-width: 36%;
+ margin-left: auto;
+ padding-left: 8px;
+ color: #9aa0a6;
+ font-size: 11px !important;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: right;
+ user-select: text;
+ -webkit-user-select: text;
+}
+
+.pw-console-row:hover .pw-console-src {
+ color: #8ab4f8;
+}
+
+.pw-console-src-empty {
+ min-width: 0;
+ padding: 0;
+}
+
+.pw-console-stack-spacer {
+ flex: 0 0 10px;
+ width: 10px;
+}
+
+.pw-console-stack-toggle {
+ flex: 0 0 10px;
+ margin-top: 2px;
+}
+
.pw-console-toggle {
cursor: pointer;
display: inline-flex;
align-items: flex-start;
gap: 3px;
+ user-select: none;
+ -webkit-user-select: none;
}
.pw-console-caret {
@@ -843,8 +975,10 @@ body {
word-break: break-word;
}
+/* %c 内联样式优先;无样式时跟控制台默认前景色 */
.pw-v-fmt { color: inherit; }
.pw-v-text { color: #e8eaed; }
+/* 仅对象属性展开时的字符串值(带引号),消息参数用 .pw-v-text */
.pw-v-string { color: #f28b82; }
.pw-v-number { color: #a8c7fa; }
.pw-v-boolean { color: #c58af9; }
@@ -864,39 +998,42 @@ body {
padding: 4px 6px;
background: #2a2a2a;
border-bottom: 1px solid var(--pw-border);
- cursor: move;
+ cursor: grab;
flex-shrink: 0;
+ user-select: none;
+ touch-action: none;
+}
+
+.pw-float-head:active {
+ cursor: grabbing;
}
.pw-float-title {
color: #fff;
font-weight: 600;
- cursor: grab;
- user-select: none;
padding: 2px 4px;
border-radius: 3px;
-}
-
-.pw-float-title:hover {
- background: #3a3a3a;
-}
-
-.pw-float-title:active {
- cursor: grabbing;
+ pointer-events: none;
}
.pw-float-hint {
color: var(--pw-muted);
font-size: 10px;
margin-left: auto;
+ pointer-events: none;
}
.pw-float-close {
flex-shrink: 0;
+ cursor: pointer;
+}
+
+.pw-float-dock-dragging,
+.pw-float-dragging {
+ opacity: 0.85;
}
-.pw-float-dock-dragging {
- opacity: 0.4;
+.pw-float-dragging {
pointer-events: none !important;
}
@@ -906,6 +1043,8 @@ body {
overflow: hidden;
display: flex;
flex-direction: column;
+ cursor: default;
+ touch-action: auto;
}
.pw-float-body .pw-panel-root,
@@ -1005,21 +1144,24 @@ body {
.pw-insp-section.open .pw-insp-sec-body { display: block; }
.pw-insp-row {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 6px;
+ display: grid;
+ grid-template-columns: minmax(96px, 0.95fr) minmax(110px, 1.25fr);
+ align-items: start;
+ column-gap: 10px;
+ margin-bottom: 7px;
min-height: 22px;
}
.pw-insp-label {
- flex: 0 0 52px;
- color: #9d9d9d;
+ color: #b0b0b0;
font-size: 11px;
- text-align: right;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ line-height: 1.35;
+ text-align: left;
+ padding-top: 3px;
+ word-break: break-word;
+ overflow-wrap: anywhere;
+ hyphens: auto;
+ user-select: text;
}
.pw-insp-input {
@@ -1090,6 +1232,7 @@ body {
position: relative;
width: 28px;
height: 16px;
+ margin-top: 3px;
cursor: pointer;
}
@@ -1137,6 +1280,62 @@ body {
padding: 4px 0;
}
+.pw-insp-select {
+ cursor: pointer;
+}
+
+.pw-insp-ref {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ height: 22px;
+ padding: 0 6px;
+ border: 1px solid #2f5d50;
+ border-radius: 2px;
+ background: #1a2e28;
+ color: #9cdcfe;
+ overflow: hidden;
+}
+
+.pw-insp-ref-kind {
+ flex: 0 0 auto;
+ color: #4ec9b0;
+ font-size: 10px;
+}
+
+.pw-insp-ref-name {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.pw-insp-color {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.pw-insp-color-picker {
+ width: 28px;
+ height: 22px;
+ padding: 0;
+ border: 1px solid #3c3c3c;
+ border-radius: 2px;
+ background: transparent;
+ cursor: pointer;
+}
+
+.pw-insp-color-hex {
+ flex: 1;
+ min-width: 0;
+}
+
/* —— 可拖拽停靠 —— */
.pw-dock {
display: flex;
diff --git a/preview-template/workbench/js/agent.js b/preview-template/workbench/js/agent.js
index 8fbe3e2..dc62406 100644
--- a/preview-template/workbench/js/agent.js
+++ b/preview-template/workbench/js/agent.js
@@ -25,11 +25,16 @@
this._overlay = null;
this._consoleOrig = null;
this._consoleBuf = [];
+ this._consoleHist = [];
this._consoleFlushTimer = 0;
this._consoleObjs = {};
this._consoleObjSeq = 1;
+ this._consoleErrBound = false;
+ this._consolePost = false;
this._hierSnap = null;
this._boundMessage = this._onMessage.bind(this);
+ this._boundConsoleError = this._onConsoleError.bind(this);
+ this._boundConsoleRejection = this._onConsoleRejection.bind(this);
this._sampler = null;
this._markers = null;
this._device = null;
@@ -44,6 +49,8 @@
global.PWDevicePreview.hideOfficialToolbar();
}
window.addEventListener('message', this._boundMessage);
+ // 尽早静默缓冲,订阅控制台后再推送到壳(避免打开面板前日志丢失)
+ this._hookConsole(true);
this._ensureOverlay();
this._waitForEngine(function (cc) {
self.cc = cc;
@@ -178,13 +185,17 @@
WorkbenchAgent.prototype.hibernate = function () {
this._hibernating = true;
+ this._consolePost = false;
for (var i = 0; i < CHANNELS.length; i++) this._onChannelOff(CHANNELS[i]);
+ this._hookConsole(false);
+ this._consoleHist.length = 0;
this._post({ type: 'event', name: 'agent.hibernate', data: { on: true } });
return true;
};
WorkbenchAgent.prototype.wake = function () {
this._hibernating = false;
+ this._hookConsole(true);
for (var i = 0; i < CHANNELS.length; i++) {
if (this._subs[CHANNELS[i]] > 0) this._onChannelOn(CHANNELS[i]);
}
@@ -217,6 +228,8 @@
}, ASSETS_DEFAULT_MS);
} else if (ch === 'console') {
this._hookConsole(true);
+ this._consolePost = true;
+ this._flushConsoleHist();
} else if (ch === 'pick') {
this._bindCanvasPick(true);
this._clearTimer('pickRect');
@@ -239,7 +252,13 @@
} else if (ch === 'assets') {
this._clearTimer('assets');
} else if (ch === 'console') {
- this._hookConsole(false);
+ // 保持静默缓冲,仅停止推送;hibernate 时才真正卸 hook
+ this._consolePost = false;
+ if (this._consoleFlushTimer) {
+ clearTimeout(this._consoleFlushTimer);
+ this._consoleFlushTimer = 0;
+ }
+ this._consoleBuf.length = 0;
} else if (ch === 'pick') {
this._bindCanvasPick(false);
this._clearTimer('pickRect');
@@ -357,6 +376,170 @@
return { uuid: this._selectedUuid, ok: !!node };
};
+ /** 组件内部属性,不在 Inspector 展示 */
+ var COMP_PROP_SKIP = {
+ _name: 1, _objFlags: 1, __scriptAsset: 1, node: 1, _enabled: 1,
+ __prefab: 1, __editorExtras__: 1, uuid: 1, name: 1, enabled: 1,
+ };
+
+ /**
+ * 读取 CCClass 属性元数据(displayName / type / enumList / visible)。
+ * @param {Function} ctor
+ * @returns {object}
+ */
+ function getCtorAttrs(ctor) {
+ if (!ctor) return {};
+ if (ctor.__attrs__ && typeof ctor.__attrs__ === 'object') return ctor.__attrs__;
+ return {};
+ }
+
+ /**
+ * @param {object} attrs
+ * @param {string} key
+ * @param {string} name
+ * @returns {*}
+ */
+ function attrOf(attrs, key, name) {
+ return attrs[key + '$_$' + name];
+ }
+
+ /**
+ * 列出组件应展示的属性名(对齐编辑器:走 __props__,隐藏 _ 开头与 visible:false)。
+ * @param {Function} ctor
+ * @returns {string[]}
+ */
+ function listCompPropKeys(ctor) {
+ var attrs = getCtorAttrs(ctor);
+ var raw = (ctor && (ctor.__props__ || ctor.__values__)) || [];
+ var out = [];
+ for (var i = 0; i < raw.length; i++) {
+ var key = raw[i];
+ if (!key || COMP_PROP_SKIP[key]) continue;
+ if (key.charAt(0) === '_' || key.indexOf('__') === 0) continue;
+ if (attrOf(attrs, key, 'visible') === false) continue;
+ out.push(key);
+ }
+ return out;
+ }
+
+ /**
+ * 将运行时值序列化为 Inspector DTO。
+ * @param {*} val
+ * @param {object} attrs
+ * @param {string} key
+ * @param {object} cc
+ * @returns {{type:string, value:*, enumOptions?:Array, readOnly?:boolean}}
+ */
+ function serializeFieldValue(val, attrs, key, cc) {
+ var enumList = attrOf(attrs, key, 'enumList');
+ var typeTag = attrOf(attrs, key, 'type');
+ if (enumList && enumList.length) {
+ return {
+ type: 'enum',
+ value: val,
+ enumOptions: enumList.map(function (it) {
+ return { name: String(it.name), value: it.value };
+ }),
+ };
+ }
+ if (typeTag === 'Enum') {
+ return { type: 'number', value: typeof val === 'number' ? val : Number(val) || 0 };
+ }
+ var t = typeof val;
+ if (t === 'boolean' || t === 'string') return { type: t, value: val };
+ if (t === 'number') return { type: 'number', value: isFinite(val) ? val : 0 };
+ if (val == null) {
+ var ctorHint = attrOf(attrs, key, 'ctor');
+ var hintName = ctorHint && (ctorHint.name || ctorHint.prototype && ctorHint.prototype.__classname__);
+ if (hintName || typeTag === 'Object') {
+ return {
+ type: 'ref',
+ readOnly: true,
+ value: { kind: hintName || 'Object', name: '(空)', uuid: '' },
+ };
+ }
+ return { type: 'string', value: '' };
+ }
+ if (cc && cc.Color && val instanceof cc.Color) {
+ return {
+ type: 'color',
+ value: { r: val.r | 0, g: val.g | 0, b: val.b | 0, a: val.a != null ? (val.a | 0) : 255 },
+ };
+ }
+ // Size
+ if (typeof val.width === 'number' && typeof val.height === 'number' && val.x === undefined) {
+ return { type: 'size', value: { width: val.width, height: val.height } };
+ }
+ // Vec2 / Vec3
+ if (typeof val.x === 'number' && typeof val.y === 'number') {
+ if (typeof val.z === 'number') {
+ return { type: 'vec3', value: { x: val.x, y: val.y, z: val.z } };
+ }
+ return { type: 'vec2', value: { x: val.x, y: val.y } };
+ }
+ // Asset / Node / Component 引用
+ if (typeof val === 'object') {
+ var kind = (val.constructor && val.constructor.name) || 'Object';
+ var name = '';
+ if (val.name != null) name = String(val.name);
+ else if (val.node && val.node.name) name = String(val.node.name);
+ var uuid = val._uuid || val.uuid || '';
+ return {
+ type: 'ref',
+ readOnly: true,
+ value: { kind: kind, name: name || kind, uuid: uuid ? String(uuid) : '' },
+ };
+ }
+ return { type: 'string', value: String(val) };
+ }
+
+ /**
+ * 提取单个组件的 Inspector 字段列表。
+ * @param {object} comp
+ * @param {number} index
+ * @param {object} cc
+ * @returns {{name:string, index:number, fields:Array}}
+ */
+ function extractCompFields(comp, index, cc) {
+ var ctor = comp.constructor;
+ var cname = (cc && cc.js && typeof cc.js.getClassName === 'function')
+ ? (cc.js.getClassName(ctor) || ctor.name)
+ : (ctor && ctor.name) || 'Component';
+ var attrs = getCtorAttrs(ctor);
+ var keys = listCompPropKeys(ctor);
+ // 无元数据时有限回退
+ if (!keys.length) {
+ var own = Object.keys(comp);
+ for (var o = 0; o < own.length && keys.length < 24; o++) {
+ var ok = own[o];
+ if (!ok || ok.charAt(0) === '_' || COMP_PROP_SKIP[ok]) continue;
+ var ot = typeof comp[ok];
+ if (ot === 'number' || ot === 'boolean' || ot === 'string') keys.push(ok);
+ }
+ if (!keys.length) {
+ console.warn('[Agent] 组件无 CCClass 属性元数据', cname);
+ }
+ }
+ var fields = [];
+ var max = 80;
+ for (var i = 0; i < keys.length && fields.length < max; i++) {
+ var key = keys[i];
+ var label = attrOf(attrs, key, 'displayName') || key;
+ var raw;
+ try { raw = comp[key]; } catch (e) { continue; }
+ var ser = serializeFieldValue(raw, attrs, key, cc);
+ fields.push({
+ key: key,
+ label: label,
+ type: ser.type,
+ value: ser.value,
+ enumOptions: ser.enumOptions || null,
+ readOnly: !!ser.readOnly,
+ });
+ }
+ return { name: cname, index: index, fields: fields };
+ }
+
WorkbenchAgent.prototype._getNodeProps = function (uuid) {
var node = this._findNode(uuid);
if (!node) return null;
@@ -364,28 +547,12 @@
var p = node.getPosition ? node.getPosition() : node.position;
var e = node.eulerAngles || { x: 0, y: 0, z: 0 };
var s = node.getScale ? node.getScale() : node.scale;
- var uit = node.getComponent && cc ? node.getComponent(cc.UITransform) : null;
var comps = [];
if (node.components) {
for (var i = 0; i < node.components.length; i++) {
var comp = node.components[i];
if (!comp || !comp.constructor) continue;
- var cname = comp.constructor.name;
- if (cname === 'UITransform') continue;
- var fields = [];
- var keys = Object.keys(comp);
- var shown = 0;
- for (var k = 0; k < keys.length && shown < 24; k++) {
- var key = keys[k];
- if (!key || key.charAt(0) === '_' || key === 'node' || key === 'uuid') continue;
- var val = comp[key];
- var t = typeof val;
- if (t === 'number' || t === 'boolean' || t === 'string') {
- fields.push({ key: key, type: t, value: val });
- shown++;
- }
- }
- comps.push({ name: cname, index: i, fields: fields });
+ comps.push(extractCompFields(comp, i, cc));
}
}
return {
@@ -396,10 +563,6 @@
pos: { x: p.x, y: p.y, z: p.z },
euler: { x: e.x, y: e.y, z: e.z },
scale: { x: s.x, y: s.y, z: s.z },
- ui: uit ? {
- width: uit.width, height: uit.height,
- anchorX: uit.anchorX, anchorY: uit.anchorY,
- } : null,
comps: comps,
};
};
@@ -408,7 +571,6 @@
var node = this._findNode(uuid);
if (!node || !fid) return false;
var cc = this.cc;
- var uit = node.getComponent && cc ? node.getComponent(cc.UITransform) : null;
try {
if (fid === 'name') node.name = String(value);
else if (fid === 'active') node.active = !!value;
@@ -434,24 +596,132 @@
var sz = fid === 'sz' ? Number(value) : sc.z;
if (typeof node.setScale === 'function') node.setScale(sx, sy, sz);
else if (cc && cc.Vec3) node.scale = new cc.Vec3(sx, sy, sz);
- } else if (uit && (fid === 'uiw' || fid === 'uih' || fid === 'uax' || fid === 'uay')) {
- if (fid === 'uiw') uit.width = Number(value);
- if (fid === 'uih') uit.height = Number(value);
- if (fid === 'uax') uit.anchorX = Number(value);
- if (fid === 'uay') uit.anchorY = Number(value);
} else if (fid.indexOf('c') === 0 && fid.indexOf('_') > 0) {
- var parts = fid.split('_');
- var ci = parseInt(parts[0].slice(1), 10);
- var key = parts.slice(1).join('_');
- if (node.components && node.components[ci]) node.components[ci][key] = value;
+ return this._setCompProp(node, fid, value);
+ } else {
+ return false;
}
return true;
- } catch (e) {
- console.warn('[Agent] set prop', fid, e);
+ } catch (err) {
+ console.warn('[Agent] set prop', fid, err);
+ return false;
+ }
+ };
+
+ /**
+ * 写回组件字段。fid: c{index}_{key}[_axis]
+ * @param {object} node
+ * @param {string} fid
+ * @param {*} value
+ * @returns {boolean}
+ */
+ WorkbenchAgent.prototype._setCompProp = function (node, fid, value) {
+ var m = /^c(\d+)_(.+)$/.exec(fid);
+ if (!m) return false;
+ var ci = parseInt(m[1], 10);
+ var rest = m[2];
+ var comp = node.components && node.components[ci];
+ if (!comp) return false;
+ var cc = this.cc;
+ var axis = '';
+ var key = rest;
+ var axisMatch = /^(.*)_(x|y|z|w|h|r|g|b|a|hex)$/.exec(rest);
+ if (axisMatch) {
+ // 优先整键命中(字段名本身含下划线时仍以整键为准)
+ if (!(rest in comp) && (axisMatch[1] in comp || hasCompProp(comp, axisMatch[1]))) {
+ key = axisMatch[1];
+ axis = axisMatch[2];
+ }
+ }
+ if (axis) {
+ return setCompAxisValue(comp, key, axis, value, cc);
+ }
+ var attrs = getCtorAttrs(comp.constructor);
+ if (attrOf(attrs, key, 'type') === 'Object' || attrOf(attrs, key, 'ctor')) {
+ // 引用只读
+ return false;
+ }
+ var cur;
+ try { cur = comp[key]; } catch (e1) { return false; }
+ if (cur && typeof cur === 'object' && !(typeof cur === 'boolean')) {
+ // 未带轴的对象写入拒绝(避免覆盖引用)
+ if (typeof cur.x === 'number' || typeof cur.width === 'number' || (cc && cc.Color && cur instanceof cc.Color)) {
+ return false;
+ }
+ }
+ try {
+ if (typeof cur === 'boolean') comp[key] = !!value;
+ else if (typeof cur === 'number') comp[key] = Number(value);
+ else if (typeof cur === 'string') comp[key] = String(value);
+ else if (attrOf(attrs, key, 'enumList')) comp[key] = Number(value);
+ else comp[key] = value;
+ return true;
+ } catch (e2) {
+ console.warn('[Agent] set comp prop', key, e2);
return false;
}
};
+ /**
+ * @param {object} comp
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function hasCompProp(comp, key) {
+ if (!comp || !key) return false;
+ if (key in comp) return true;
+ var ctor = comp.constructor;
+ var props = (ctor && (ctor.__props__ || ctor.__values__)) || [];
+ return props.indexOf(key) >= 0;
+ }
+
+ /**
+ * @param {object} comp
+ * @param {string} key
+ * @param {string} axis
+ * @param {*} value
+ * @param {object} cc
+ * @returns {boolean}
+ */
+ function setCompAxisValue(comp, key, axis, value, cc) {
+ var cur;
+ try { cur = comp[key]; } catch (e) { return false; }
+ if (axis === 'hex') {
+ var hex = String(value).replace(/^#/, '');
+ if (hex.length < 6) return false;
+ var r = parseInt(hex.slice(0, 2), 16);
+ var g = parseInt(hex.slice(2, 4), 16);
+ var b = parseInt(hex.slice(4, 6), 16);
+ var a = hex.length >= 8 ? parseInt(hex.slice(6, 8), 16) : 255;
+ if (cc && cc.Color) {
+ comp[key] = new cc.Color(r, g, b, a);
+ return true;
+ }
+ if (cur && typeof cur.r === 'number') {
+ cur.r = r; cur.g = g; cur.b = b; cur.a = a;
+ return true;
+ }
+ return false;
+ }
+ var num = Number(value);
+ if (!isFinite(num)) return false;
+ if (!cur || typeof cur !== 'object') return false;
+ if (axis === 'w' && typeof cur.width === 'number') { cur.width = num; return true; }
+ if (axis === 'h' && typeof cur.height === 'number') { cur.height = num; return true; }
+ if ((axis === 'x' || axis === 'y' || axis === 'z') && typeof cur[axis] === 'number') {
+ cur[axis] = num;
+ // 触发 setter(部分引擎属性需整体赋值)
+ try { comp[key] = cur; } catch (e2) { /* 就地改写已足够 */ }
+ return true;
+ }
+ if ((axis === 'r' || axis === 'g' || axis === 'b' || axis === 'a') && typeof cur[axis] === 'number') {
+ cur[axis] = num;
+ try { comp[key] = cur; } catch (e3) { /* */ }
+ return true;
+ }
+ return false;
+ }
+
WorkbenchAgent.prototype._listAssets = function () {
var cc = this.cc;
if (!cc || !cc.assetManager) return { rows: [], bundles: [] };
@@ -634,40 +904,100 @@
console.warn = wrap('warn', this._consoleOrig.warn);
console.error = wrap('error', this._consoleOrig.error);
console.debug = wrap('debug', this._consoleOrig.debug);
+ this._bindConsoleErrors(true);
} else if (this._consoleOrig) {
+ this._bindConsoleErrors(false);
console.log = this._consoleOrig.log;
console.info = this._consoleOrig.info;
console.warn = this._consoleOrig.warn;
console.error = this._consoleOrig.error;
console.debug = this._consoleOrig.debug;
this._consoleOrig = null;
+ this._consolePost = false;
if (this._consoleFlushTimer) {
clearTimeout(this._consoleFlushTimer);
this._consoleFlushTimer = 0;
}
- this._flushConsole();
+ this._consoleBuf.length = 0;
+ }
+ };
+
+ /** 挂载/卸载 window 未捕获异常监听(随 console 通道启停) */
+ WorkbenchAgent.prototype._bindConsoleErrors = function (on) {
+ if (on) {
+ if (this._consoleErrBound) return;
+ window.addEventListener('error', this._boundConsoleError);
+ window.addEventListener('unhandledrejection', this._boundConsoleRejection);
+ this._consoleErrBound = true;
+ } else if (this._consoleErrBound) {
+ window.removeEventListener('error', this._boundConsoleError);
+ window.removeEventListener('unhandledrejection', this._boundConsoleRejection);
+ this._consoleErrBound = false;
+ }
+ };
+
+ /** 脚本运行时错误 → 控制台 error */
+ WorkbenchAgent.prototype._onConsoleError = function (ev) {
+ if (!ev) return;
+ var args = ev.error instanceof Error ? [ev.error] : [ev.message || 'Script error'];
+ var entry = this._serializeConsoleArgs('error', args);
+ if (ev.filename) {
+ entry.src = this._srcFromUrl(ev.filename, ev.lineno || 0, ev.colno || 0);
}
+ this._enqueueConsole(entry);
+ };
+
+ /** 未处理 Promise 拒绝 → 对齐浏览器 Uncaught (in promise) */
+ WorkbenchAgent.prototype._onConsoleRejection = function (ev) {
+ var reason = ev && ev.reason;
+ var args;
+ if (reason instanceof Error) {
+ args = ['Uncaught (in promise)', reason];
+ } else {
+ args = ['Uncaught (in promise) ' + String(reason)];
+ }
+ this._pushConsole('error', args);
};
WorkbenchAgent.prototype._pushConsole = function (level, args) {
- var entry = this._serializeConsoleArgs(level, args);
- this._consoleBuf.push(entry);
- if (this._consoleBuf.length > 200) this._consoleBuf.splice(0, this._consoleBuf.length - 200);
- var self = this;
- if (!this._consoleFlushTimer) {
- this._consoleFlushTimer = setTimeout(function () {
- self._consoleFlushTimer = 0;
- self._flushConsole();
- }, CONSOLE_FLUSH_MS);
+ this._enqueueConsole(this._serializeConsoleArgs(level, args));
+ };
+
+ WorkbenchAgent.prototype._enqueueConsole = function (entry) {
+ if (!entry) return;
+ if (this._consolePost && !this._hibernating) {
+ this._consoleBuf.push(entry);
+ var self = this;
+ if (!this._consoleFlushTimer) {
+ this._consoleFlushTimer = setTimeout(function () {
+ self._consoleFlushTimer = 0;
+ self._flushConsole();
+ }, CONSOLE_FLUSH_MS);
+ }
+ return;
}
+ // 未订阅时静默环缓冲,打开面板时再推送
+ this._consoleHist.push(entry);
+ if (this._consoleHist.length > 200) this._consoleHist.splice(0, this._consoleHist.length - 200);
};
WorkbenchAgent.prototype._flushConsole = function () {
- if (!this._consoleBuf.length) return;
+ if (!this._consolePost || !this._consoleBuf.length) {
+ this._consoleBuf.length = 0;
+ return;
+ }
var batch = this._consoleBuf.splice(0, this._consoleBuf.length);
this._post({ type: 'event', name: 'console.batch', data: { items: batch } });
};
+ /** 订阅时把未推送历史一次性推给壳 */
+ WorkbenchAgent.prototype._flushConsoleHist = function () {
+ if (!this._consolePost || !this._consoleHist.length) return;
+ var batch = this._consoleHist.splice(0, this._consoleHist.length);
+ this._post({ type: 'event', name: 'console.batch', data: { items: batch } });
+ this._consoleBuf.length = 0;
+ };
+
/**
* 将 console 参数序列化为可 postMessage 的富结构(保留 %c/%s 等,对象可展开)。
* 原始 console.* 仍会调用,浏览器 DevTools 打印不受影响。
@@ -676,28 +1006,124 @@
var parts = [];
var plain = '';
var FMT_RE = /%(?:%|[csdifoO])/;
+ var rawStack = '';
+ try { rawStack = new Error().stack || ''; } catch (e0) { rawStack = ''; }
if (args.length && typeof args[0] === 'string' && FMT_RE.test(args[0])) {
var formatted = this._formatConsole(args);
parts = formatted.parts;
plain = formatted.plain;
+ // 对齐 Chrome:%c 只作用于格式串本身;其后附加参数用默认色、不加引号
for (var r = 0; r < formatted.rest.length; r++) {
- var rp = this._valuePart(formatted.rest[r]);
- parts.push(rp);
- plain += (plain ? ' ' : '') + (rp.plain || '');
+ var rv = formatted.rest[r];
+ if (typeof rv === 'string') {
+ parts.push({ kind: 'text', text: rv, plain: rv });
+ plain += (plain ? ' ' : '') + rv;
+ } else {
+ var rp = this._valuePart(rv);
+ parts.push(rp);
+ plain += (plain ? ' ' : '') + (rp.plain || '');
+ }
}
} else {
for (var i = 0; i < args.length; i++) {
- var p = this._valuePart(args[i], i === 0 && typeof args[i] === 'string');
+ // 顶层消息字符串不加引号(属性展开时仍用 string 带引号)
+ var p = this._valuePart(args[i], typeof args[i] === 'string');
parts.push(p);
plain += (i ? ' ' : '') + (p.plain || '');
}
}
+ var src = this._srcFromStack(rawStack);
+ var callStack = '';
+ for (var j = 0; j < parts.length; j++) {
+ if (parts[j] && parts[j].kind === 'error' && parts[j].stack) {
+ if (!src) src = this._srcFromStack(parts[j].stack);
+ callStack = parts[j].stack;
+ break;
+ }
+ }
+ if (!callStack && (level === 'warn' || level === 'error')) {
+ callStack = this._businessStack(rawStack);
+ }
return {
id: 'c' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
level: level,
t: Date.now(),
parts: parts,
plain: plain,
+ src: src,
+ stack: callStack || '',
+ };
+ };
+
+ /** 去掉 agent 帧后的业务堆栈文本 */
+ WorkbenchAgent.prototype._businessStack = function (stack) {
+ if (!stack) return '';
+ var lines = String(stack).split('\n');
+ var out = [];
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i];
+ var t = line.trim();
+ if (!t) continue;
+ if (t.indexOf('Error') === 0 && t.indexOf('at ') < 0) continue;
+ if (this._isAgentStackLine(t)) continue;
+ out.push(line);
+ }
+ return out.join('\n');
+ };
+
+ WorkbenchAgent.prototype._isAgentStackLine = function (line) {
+ return /agent\.js|WorkbenchAgent|_pushConsole|_enqueueConsole|_serializeConsole|_captureConsoleSrc|_srcFromStack|_businessStack|_hookConsole|_onConsoleError|_onConsoleRejection|_valuePart|_formatConsole|_flushConsole/i.test(line);
+ };
+
+ /** 解析 stack:优先可读源文件名,跳过 agent / UUID 哈希帧 */
+ WorkbenchAgent.prototype._srcFromStack = function (stack) {
+ if (!stack) return null;
+ var lines = String(stack).split('\n');
+ var best = null;
+ var bestScore = -1;
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i].trim();
+ if (!line || /^error$/i.test(line) || (line.indexOf('Error') === 0 && line.indexOf('at ') < 0)) continue;
+ if (this._isAgentStackLine(line)) continue;
+ var m = line.match(/\((.+?):(\d+):(\d+)\)\s*$/) || line.match(/at\s+(\S+?):(\d+):(\d+)\s*$/);
+ if (!m) continue;
+ var cand = this._srcFromUrl(m[1], parseInt(m[2], 10) || 0, parseInt(m[3], 10) || 0);
+ var score = this._scoreSrcFile(cand.file);
+ if (score > bestScore) {
+ bestScore = score;
+ best = cand;
+ }
+ }
+ return best;
+ };
+
+ /** 来源文件可读性评分(越高越优先展示) */
+ WorkbenchAgent.prototype._scoreSrcFile = function (file) {
+ var f = String(file || '');
+ if (!f || f === '(anonymous)') return 0;
+ if (/^[a-f0-9]{16,}$/i.test(f) || /^[a-f0-9-]{32,}$/i.test(f)) return 1;
+ if (/\.ts$/i.test(f)) return 50;
+ if (/\.js$/i.test(f) && /[a-zA-Z]/.test(f) && !/^[a-f0-9.-]+$/i.test(f.replace(/\.js$/i, ''))) return 40;
+ if (/\.(js|mjs|cjs)$/i.test(f)) return 20;
+ if (/[a-zA-Z]/.test(f)) return 10;
+ return 2;
+ };
+
+ /** URL + 行列 → 简短来源 */
+ WorkbenchAgent.prototype._srcFromUrl = function (url, line, col) {
+ var u = String(url || '');
+ var file = u.split('/').pop() || u;
+ var q = file.indexOf('?');
+ if (q >= 0) file = file.slice(0, q);
+ var h = file.indexOf('#');
+ if (h >= 0) file = file.slice(0, h);
+ if (!file) file = '(anonymous)';
+ var ln = line || 0;
+ return {
+ file: file,
+ line: ln,
+ col: col || 0,
+ text: ln ? (file + ':' + ln) : file,
};
};
@@ -751,7 +1177,7 @@
plain += lit;
}
}
- return { parts: parts, plain: plain, rest: args.slice(ai) };
+ return { parts: parts, plain: plain, rest: args.slice(ai), style: style };
};
WorkbenchAgent.prototype._valuePart = function (value, bareString) {
diff --git a/preview-template/workbench/js/host.js b/preview-template/workbench/js/host.js
index 757f542..683009e 100644
--- a/preview-template/workbench/js/host.js
+++ b/preview-template/workbench/js/host.js
@@ -125,6 +125,46 @@
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);
@@ -291,6 +331,13 @@
});
}
+ 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 () {
diff --git a/preview-template/workbench/js/layout.js b/preview-template/workbench/js/layout.js
index 37e7d66..f0c99f9 100644
--- a/preview-template/workbench/js/layout.js
+++ b/preview-template/workbench/js/layout.js
@@ -652,32 +652,117 @@
WorkbenchLayout.prototype._onDocDragOver = function (ev) {
if (!this._dragPanel) return;
- var leafEl = ev.target.closest && ev.target.closest('.pw-leaf');
- if (!leafEl) {
+ // 按指针坐标几何命中(iframe/浮层下 ev.target 不可靠)
+ var info = this._findLeafAtClientPoint(ev.clientX, ev.clientY);
+ if (!info || !info.el) {
if (this.dropOverlay) this.dropOverlay.hidden = true;
+ this._hoverTargetId = null;
+ this._hoverEdge = null;
+ this._hoverGroup = null;
+ this._hoverGroupCols = null;
return;
}
ev.preventDefault();
- var leafId = leafEl.getAttribute('data-leaf-id');
- var info = this._leafEls[leafId];
- if (!info) return;
+ this._updateDockHoverFromPoint(ev.clientX, ev.clientY, this._dragPanel, info, ev.target);
+ };
+ /**
+ * 按客户区坐标找最深(面积最小)的 leaf;不依赖 elementFromPoint(游戏 iframe 会挡住)。
+ * @param {number} cx
+ * @param {number} cy
+ * @returns {{el:HTMLElement, node:object}|null}
+ */
+ WorkbenchLayout.prototype._findLeafAtClientPoint = function (cx, cy) {
+ var best = null;
+ var bestArea = Infinity;
+ for (var id in this._leafEls) {
+ if (!Object.prototype.hasOwnProperty.call(this._leafEls, id)) continue;
+ var info = this._leafEls[id];
+ if (!info || !info.el || !info.node) continue;
+ var r = info.el.getBoundingClientRect();
+ if (cx < r.left || cx > r.right || cy < r.top || cy > r.bottom) continue;
+ var area = Math.max(1, r.width) * Math.max(1, r.height);
+ if (area < bestArea) {
+ bestArea = area;
+ best = info;
+ }
+ }
+ return best;
+ };
+
+ /**
+ * 根据指针位置更新停靠悬停状态与蓝框。
+ * @param {number} cx
+ * @param {number} cy
+ * @param {string} panelId
+ * @param {{el:HTMLElement, node:object}|null} info
+ * @param {EventTarget=} hitTarget
+ */
+ WorkbenchLayout.prototype._updateDockHoverFromPoint = function (cx, cy, panelId, info, hitTarget) {
+ if (!info || !info.el || !info.node) {
+ this._clearDragVisual();
+ this._hoverTargetId = null;
+ this._hoverEdge = null;
+ this._hoverGroup = null;
+ this._hoverGroupCols = null;
+ return;
+ }
+ var leafEl = info.el;
var rect = leafEl.getBoundingClientRect();
- var x = (ev.clientX - rect.left) / Math.max(1, rect.width);
- var y = (ev.clientY - rect.top) / Math.max(1, rect.height);
- var edge = this._hitEdge(info.node, x, y, ev.target);
+ var x = (cx - rect.left) / Math.max(1, rect.width);
+ var y = (cy - rect.top) / Math.max(1, rect.height);
+ var edge = this._hitEdge(info.node, x, y, hitTarget || null);
+ // 仅工作区左右外缘才整树新建列(勿把任意 leaf 的左右边误当成整树)
+ var ws = this.workspace ? this.workspace.getBoundingClientRect() : null;
+ if (ws && ws.width > 40) {
+ var margin = Math.min(48, ws.width * 0.08);
+ var atRight = cx >= ws.right - margin;
+ var atLeft = cx <= ws.left + margin;
+ if (atRight || atLeft) {
+ var rootEdge = this._resolveWorkspaceSideDrop(atRight ? 'right' : 'left');
+ if (rootEdge) {
+ this._hoverTargetId = '__workspace__';
+ this._hoverEdge = rootEdge.edge;
+ this._hoverGroup = null;
+ this._hoverGroupCols = null;
+ this._paintDockHover(rootEdge, leafEl);
+ return;
+ }
+ }
+ }
var resolved = this._resolveDropTarget(info.node, edge);
-
this._hoverTargetId = resolved.node ? resolved.node.id : null;
this._hoverEdge = resolved.edge;
this._hoverGroup = resolved.group || null;
this._hoverGroupCols = (resolved.group && resolved.group.length)
- ? this._snapshotGroupColumns(resolved.group, this._dragPanel)
+ ? this._snapshotGroupColumns(resolved.group, panelId)
: null;
+ this._paintDockHover(resolved, leafEl);
+ };
+ /**
+ * 工作区左右外缘 → 相对整棵树新建列。
+ * @param {string} edge
+ * @returns {{node:object, edge:string, group:null}|null}
+ */
+ WorkbenchLayout.prototype._resolveWorkspaceSideDrop = function (edge) {
+ if (!this._tree) return null;
+ this._ensureId(this._tree);
+ // node 仅用于绘制蓝框;真正落点用 __workspace__ 哨兵,避免 cleanup 丢 id 后找不到
+ return { node: this._tree, edge: edge, group: null };
+ };
+
+ /**
+ * 绘制停靠蓝框。
+ * @param {{node:object|null, edge:string, group:Array|null}} resolved
+ * @param {HTMLElement} fallbackEl
+ */
+ WorkbenchLayout.prototype._paintDockHover = function (resolved, fallbackEl) {
if (resolved.group && resolved.group.length) {
this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false);
- } else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') {
+ return;
+ }
+ if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') {
var splitInfo = this._nodeEls[resolved.node.id];
if (splitInfo && splitInfo.el) {
var sr = splitInfo.el.getBoundingClientRect();
@@ -688,14 +773,30 @@
width: sr.width,
height: sr.height,
}, resolved.edge), 'region', false);
- } else {
- this._showOverlay(leafEl, resolved.edge, false);
+ return;
}
- } else {
- var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null;
- var targetEl = targetInfo ? targetInfo.el : leafEl;
- this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game');
}
+ var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null;
+ var targetEl = targetInfo && targetInfo.el ? targetInfo.el : fallbackEl;
+ if (!targetEl && resolved.node === this._tree && this.workspace) {
+ targetEl = this.workspace;
+ }
+ if (!targetEl) {
+ this._clearDragVisual();
+ return;
+ }
+ if (resolved.node === this._tree && (resolved.edge === 'left' || resolved.edge === 'right')) {
+ var wr = this.workspace.getBoundingClientRect();
+ var rr = this.root.getBoundingClientRect();
+ this._showOverlayRect(this._edgeStripRect({
+ left: wr.left - rr.left,
+ top: wr.top - rr.top,
+ width: wr.width,
+ height: wr.height,
+ }, resolved.edge), 'region', false);
+ return;
+ }
+ this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game');
};
/** 取目标矩形某一侧的新建区预览条带 */
@@ -914,10 +1015,9 @@
};
WorkbenchLayout.prototype._applyDrop = function (panelId, targetId, edge, group, groupCols) {
- this._closeFloat(panelId, false);
-
// 组停靠:用「列内容快照」而非节点引用,避免 remove/cleanup 后 id 失效导致只包一列
if (edge !== 'center' && ((groupCols && groupCols.length) || (group && group.length))) {
+ this._closeFloat(panelId, false);
var cols = (groupCols && groupCols.length)
? groupCols
: this._snapshotGroupColumns(group, panelId);
@@ -936,24 +1036,45 @@
return;
}
+ // 整树左右外侧:直接相对当前根拆分,不依赖可能被 cleanup 丢掉的根 id
+ if (targetId === '__workspace__' && (edge === 'left' || edge === 'right')) {
+ this._closeFloat(panelId, false);
+ this._removePanel(panelId);
+ this._splitBeside(this._tree, edge, panelId);
+ this._dirty = true;
+ this._tree = this._cleanup(this._tree) || this._presetTree('minimal');
+ this.rebuild();
+ this._rememberPanelFromTree(panelId);
+ this._save();
+ return;
+ }
+
var target = this._findNodeById(this._tree, targetId);
- if (!target) return;
+ if (!target) {
+ console.warn('[调试工作台] 停靠失败:找不到目标', targetId, edge);
+ return;
+ }
if (target.type === 'tabs' && (target.panels || []).indexOf(panelId) >= 0 && edge === 'center') {
+ this._closeFloat(panelId, false);
target.active = panelId;
this.rebuild();
this._save();
return;
}
+ this._closeFloat(panelId, false);
this._removePanel(panelId);
target = this._findNodeById(this._tree, targetId);
if (!target) {
- this._openFloat(panelId, null);
- this._dirty = true;
- this._save();
- return;
- }
-
- if (edge === 'center') {
+ // cleanup 可能重建了 split 节点;若原目标就是根,回退整树停靠
+ if (edge === 'left' || edge === 'right') {
+ this._splitBeside(this._tree, edge, panelId);
+ } else {
+ this._openFloat(panelId, null);
+ this._dirty = true;
+ this._save();
+ return;
+ }
+ } else if (edge === 'center') {
if (target.type === 'game' || target.type === 'split') {
this._splitBeside(target, 'bottom', panelId);
} else if (target.type === 'tabs') {
@@ -1277,7 +1398,14 @@
}
}
this._normalizeSizes(flatSizes, flatKids.length);
- return { type: 'split', dir: node.dir, sizes: flatSizes, children: flatKids };
+ // 必须保留 id:按 id 停靠(尤其是整树左右新建)依赖稳定标识
+ return {
+ type: 'split',
+ id: node.id || ('split_' + (this._leafSeq++)),
+ dir: node.dir,
+ sizes: flatSizes,
+ children: flatKids,
+ };
}
return node;
};
@@ -1463,8 +1591,8 @@
var head = document.createElement('div');
head.className = 'pw-float-head';
head.innerHTML =
- '
' +
- '
拖标题可停靠 ' +
+ '
' +
+ '
拖标题栏移动 / 停靠 ' +
'
关闭 ';
head.querySelector('.pw-float-title').textContent = PANEL_TITLES[panelId] || panelId;
var content = document.createElement('div');
@@ -1480,31 +1608,15 @@
self._closeFloat(panelId, true);
self._savePanelMemory();
});
- // 标题文字:HTML5 拖放到布局区停靠
- var title = head.querySelector('.pw-float-title');
- title.addEventListener('dragstart', function (ev) {
- self._floatDrag = null;
- win.classList.add('pw-float-dock-dragging');
- self._beginDrag(panelId, ev);
- });
- title.addEventListener('dragend', function () {
- win.classList.remove('pw-float-dock-dragging');
- self._clearDragVisual();
- self._dragPanel = null;
- self._hoverTargetId = null;
- self._hoverEdge = null;
- self._hoverGroup = null;
- self._hoverGroupCols = null;
- });
- // 标题栏空白处:移动悬浮窗位置(用当前坐标,禁止用创建时闭包坐标)
+ // 仅标题栏可拖:移动窗口;拖到停靠区松手即停靠(禁止拖面板内容)
head.addEventListener('pointerdown', function (ev) {
if (ev.target && ev.target.closest && ev.target.closest('.pw-float-close')) return;
- if (ev.target && ev.target.closest && ev.target.closest('.pw-float-title')) return;
var f = self._floats[panelId];
if (!f) return;
ev.preventDefault();
- head.setPointerCapture && head.setPointerCapture(ev.pointerId);
+ if (head.setPointerCapture) head.setPointerCapture(ev.pointerId);
self._setGameIframePassThrough(true);
+ win.classList.add('pw-float-dragging');
self._floatDrag = {
id: panelId,
ox: ev.clientX,
@@ -1533,7 +1645,6 @@
f.el.style.left = f.x + 'px';
f.el.style.top = f.y + 'px';
d.moved = true;
- // 移动时探测下方停靠区(标题拖放之外的快捷停靠)
this._probeFloatDockHover(ev.clientX, ev.clientY, d.id);
};
@@ -1544,9 +1655,17 @@
window.removeEventListener('pointerup', this._onFloatUp);
this._setGameIframePassThrough(false);
if (!d) return;
+ var win = this._floats[d.id] && this._floats[d.id].el;
+ if (win) win.classList.remove('pw-float-dragging');
+ // 使用拖动过程中最后一次悬停结果,避免松手瞬间坐标抖动导致重探测失败
+ var targetId = this._hoverTargetId;
+ var edge = this._hoverEdge;
+ var group = this._hoverGroup;
+ var groupCols = this._hoverGroupCols;
var docked = false;
- if (d.moved && this._hoverEdge && (this._hoverTargetId || (this._hoverGroupCols && this._hoverGroupCols.length))) {
- docked = this._dockFloatingPanel(d.id, ev.clientX, ev.clientY);
+ if (d.moved && edge && (targetId || (groupCols && groupCols.length) || (group && group.length))) {
+ this._applyDrop(d.id, targetId, edge, group, groupCols);
+ docked = !this._floats[d.id];
}
this._clearDragVisual();
this._dragPanel = null;
@@ -1560,22 +1679,28 @@
}
};
- /** 悬浮窗移动时:临时穿透命中下方 leaf,刷新停靠指示 */
+ /** 悬浮窗移动时:按坐标几何命中 leaf,刷新停靠指示 */
WorkbenchLayout.prototype._probeFloatDockHover = function (cx, cy, panelId) {
- var floats = this._floats;
- var ids = [];
- for (var id in floats) {
- if (Object.prototype.hasOwnProperty.call(floats, id) && floats[id].el) {
- ids.push(id);
- floats[id].el.style.pointerEvents = 'none';
+ this._dragPanel = panelId;
+ var info = this._findLeafAtClientPoint(cx, cy);
+ // 外缘停靠:即使点不在任何 leaf 上(例如右侧空白),仍允许
+ if (!info) {
+ var ws = this.workspace ? this.workspace.getBoundingClientRect() : null;
+ if (ws && ws.width > 40) {
+ var margin = Math.min(48, ws.width * 0.08);
+ if (cx >= ws.right - margin || cx <= ws.left + margin) {
+ var side = cx >= ws.right - margin ? 'right' : 'left';
+ var rootEdge = this._resolveWorkspaceSideDrop(side);
+ if (rootEdge) {
+ this._hoverTargetId = '__workspace__';
+ this._hoverEdge = rootEdge.edge;
+ this._hoverGroup = null;
+ this._hoverGroupCols = null;
+ this._paintDockHover(rootEdge, null);
+ return;
+ }
+ }
}
- }
- var el = document.elementFromPoint(cx, cy);
- for (var i = 0; i < ids.length; i++) {
- if (floats[ids[i]] && floats[ids[i]].el) floats[ids[i]].el.style.pointerEvents = '';
- }
- var leafEl = el && el.closest && el.closest('.pw-leaf');
- if (!leafEl) {
this._clearDragVisual();
this._hoverTargetId = null;
this._hoverEdge = null;
@@ -1583,40 +1708,7 @@
this._hoverGroupCols = null;
return;
}
- this._dragPanel = panelId;
- var leafId = leafEl.getAttribute('data-leaf-id');
- var info = this._leafEls[leafId];
- if (!info) return;
- var rect = leafEl.getBoundingClientRect();
- var x = (cx - rect.left) / Math.max(1, rect.width);
- var y = (cy - rect.top) / Math.max(1, rect.height);
- var edge = this._hitEdge(info.node, x, y, el);
- var resolved = this._resolveDropTarget(info.node, edge);
- this._hoverTargetId = resolved.node ? resolved.node.id : null;
- this._hoverEdge = resolved.edge;
- this._hoverGroup = resolved.group || null;
- this._hoverGroupCols = (resolved.group && resolved.group.length)
- ? this._snapshotGroupColumns(resolved.group, panelId)
- : null;
- if (resolved.group && resolved.group.length) {
- this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false);
- } else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') {
- var splitInfo = this._nodeEls[resolved.node.id];
- if (splitInfo && splitInfo.el) {
- var sr = splitInfo.el.getBoundingClientRect();
- var rootRect = this.root.getBoundingClientRect();
- this._showOverlayRect(this._edgeStripRect({
- left: sr.left - rootRect.left,
- top: sr.top - rootRect.top,
- width: sr.width,
- height: sr.height,
- }, resolved.edge), 'region', false);
- }
- } else {
- var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null;
- var targetEl = targetInfo ? targetInfo.el : leafEl;
- this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game');
- }
+ this._updateDockHoverFromPoint(cx, cy, panelId, info, null);
};
WorkbenchLayout.prototype._dockFloatingPanel = function (panelId, cx, cy) {
diff --git a/preview-template/workbench/js/panels/console.js b/preview-template/workbench/js/panels/console.js
index 047b63b..f30a945 100644
--- a/preview-template/workbench/js/panels/console.js
+++ b/preview-template/workbench/js/panels/console.js
@@ -15,6 +15,8 @@
this._bridge = null;
this._expanded = {};
this._onListClick = this._onListClick.bind(this);
+ this._onLevelClick = this._onLevelClick.bind(this);
+ this._counts = { all: 0, error: 0, warn: 0, info: 0 };
}
ConsolePanel.prototype.setBridge = function (bridge) {
@@ -26,28 +28,24 @@
var root = document.createElement('div');
root.className = 'pw-panel-inner pw-console';
root.innerHTML =
- '
' +
+ '
' +
'
控制台 ' +
- '
' +
'
';
this.el = root;
this.list = root.querySelector('#pw-console-list');
this.list.addEventListener('click', this._onListClick);
- root.querySelector('#pw-console-level').addEventListener('change', function (ev) {
- self.filter = ev.target.value;
- self.render();
- });
+ var levels = root.querySelector('.pw-console-levels');
+ if (levels) levels.addEventListener('click', this._onLevelClick);
root.querySelector('#pw-console-q').addEventListener('input', function (ev) {
self.query = ev.target.value || '';
self.render();
@@ -55,11 +53,23 @@
root.querySelector('#pw-console-clear').addEventListener('click', function () {
self.buffer.length = 0;
self._expanded = {};
+ self._recount();
self.render();
});
return root;
};
+ ConsolePanel.prototype._onLevelClick = function (ev) {
+ var btn = ev.target.closest && ev.target.closest('[data-level]');
+ if (!btn || !this.el) return;
+ this.filter = btn.getAttribute('data-level') || 'all';
+ var chips = this.el.querySelectorAll('.pw-console-chip');
+ for (var i = 0; i < chips.length; i++) {
+ chips[i].classList.toggle('active', chips[i].getAttribute('data-level') === this.filter);
+ }
+ this.render();
+ };
+
/**
* @param {Array} items Agent 序列化后的富日志
*/
@@ -67,7 +77,7 @@
if (!items || !items.length) return;
for (var i = 0; i < items.length; i++) {
var it = items[i];
- var key = it.level + '|' + (it.plain || '');
+ var key = it.level + '|' + (it.plain || '') + '|' + ((it.src && it.src.text) || '');
var last = this.buffer.length ? this.buffer[this.buffer.length - 1] : null;
if (last && last._key === key) {
last.count = (last.count || 1) + 1;
@@ -82,10 +92,37 @@
var removed = this.buffer.shift();
if (removed) delete this._expanded[removed.id];
}
+ this._recount();
this.render();
};
+ ConsolePanel.prototype._recount = function () {
+ var c = { all: 0, error: 0, warn: 0, info: 0 };
+ for (var i = 0; i < this.buffer.length; i++) {
+ var lv = this.buffer[i].level;
+ var n = this.buffer[i].count || 1;
+ c.all += n;
+ if (lv === 'error') c.error += n;
+ else if (lv === 'warn') c.warn += n;
+ else c.info += n;
+ }
+ this._counts = c;
+ if (!this.el) return;
+ var nodes = this.el.querySelectorAll('[data-count]');
+ for (var j = 0; j < nodes.length; j++) {
+ var k = nodes[j].getAttribute('data-count');
+ nodes[j].textContent = String(c[k] || 0);
+ }
+ };
+
ConsolePanel.prototype._onListClick = function (ev) {
+ var stackBtn = ev.target.closest && ev.target.closest('[data-stack]');
+ if (stackBtn) {
+ var sid = 'stack:' + (stackBtn.getAttribute('data-stack') || '');
+ this._expanded[sid] = !this._expanded[sid];
+ this.render();
+ return;
+ }
var toggle = ev.target.closest && ev.target.closest('[data-expand]');
if (!toggle) return;
var oid = toggle.getAttribute('data-oid');
@@ -114,6 +151,15 @@
}
};
+ ConsolePanel.prototype._matchFilter = function (item) {
+ if (this.filter === 'error') return item.level === 'error';
+ if (this.filter === 'warn') return item.level === 'warn';
+ if (this.filter === 'info') {
+ return item.level === 'log' || item.level === 'info' || item.level === 'debug';
+ }
+ return true;
+ };
+
ConsolePanel.prototype.render = function () {
if (!this.list) return;
var q = this.query.toLowerCase();
@@ -121,12 +167,14 @@
var any = false;
for (var i = 0; i < this.buffer.length; i++) {
var item = this.buffer[i];
- if (this.filter !== 'all' && item.level !== this.filter) continue;
+ if (!this._matchFilter(item)) continue;
if (q && String(item.plain || '').toLowerCase().indexOf(q) < 0) continue;
any = true;
html += this._renderRow(item);
}
- this.list.innerHTML = any ? html : '
暂无日志(打开本面板后开始捕获;原始输出仍在浏览器控制台)
';
+ this.list.innerHTML = any
+ ? html
+ : '
暂无日志(切到「全部」可看警告/错误;原始输出仍在浏览器控制台)
';
this.list.scrollTop = this.list.scrollHeight;
};
@@ -140,10 +188,27 @@
var count = item.count > 1
? '
' + item.count + ' '
: '';
+ var srcText = item.src && item.src.text ? item.src.text : '';
+ var src = srcText
+ ? '
' + escapeHtml(srcText) + ' '
+ : '
';
+
+ var stackKey = 'stack:' + item.id;
+ var hasStack = !!(item.stack && String(item.stack).trim());
+ var stackOpen = !!this._expanded[stackKey];
+ var stackToggle = hasStack
+ ? '
'
+ : '
';
+ var stackBlock = hasStack && stackOpen
+ ? '
' + escapeHtml(item.stack) + ' '
+ : '';
+
return (
'
' +
- icon + count +
- '
' + body + '
' +
+ stackToggle + icon + count +
+ '
' + body + stackBlock + '
' +
+ src +
'
'
);
};
@@ -178,8 +243,8 @@
return '
ƒ ' + escapeHtml(part.name || 'anonymous') + '() ';
}
if (kind === 'error') {
- var stack = part.stack ? '
' + escapeHtml(part.stack) + ' ' : '';
- return '
' + escapeHtml((part.name || 'Error') + ': ' + (part.message || '')) + ' ' + stack;
+ // 堆栈在行级折叠展示,此处只渲染消息
+ return '
' + escapeHtml((part.name || 'Error') + ': ' + (part.message || '')) + ' ';
}
if (kind === 'object') return this._renderObject(part, item);
return '
' + escapeHtml(part.plain || '') + ' ';
diff --git a/preview-template/workbench/js/panels/inspector.js b/preview-template/workbench/js/panels/inspector.js
index 386c721..f9232b6 100644
--- a/preview-template/workbench/js/panels/inspector.js
+++ b/preview-template/workbench/js/panels/inspector.js
@@ -2,7 +2,7 @@
'use strict';
/**
- * 属性面板(壳侧):通过 Bridge 读写节点属性。
+ * 属性面板(壳侧):通过 Bridge 读写节点属性;标签优先使用 displayName。
* @constructor
*/
function InspectorPanel() {
@@ -67,15 +67,6 @@
html.push(rowVec3('缩放', ['sx', 'sy', 'sz'], [p.scale.x, p.scale.y, p.scale.z]));
html.push('
');
- if (p.ui) {
- html.push('
');
- html.push('');
- html.push('');
- html.push(rowVec2('尺寸', ['uiw', 'uih'], [p.ui.width, p.ui.height], ['W', 'H']));
- html.push(rowVec2('锚点', ['uax', 'uay'], [p.ui.anchorX, p.ui.anchorY], ['X', 'Y']));
- html.push('
');
- }
-
var comps = p.comps || [];
for (var i = 0; i < comps.length; i++) {
var c = comps[i];
@@ -86,11 +77,7 @@
var fields = c.fields || [];
if (!fields.length) html.push('
无可编辑公开字段
');
for (var f = 0; f < fields.length; f++) {
- var field = fields[f];
- var fid = 'c' + c.index + '_' + field.key;
- if (field.type === 'number') html.push(rowNum(field.key, fid, field.value));
- else if (field.type === 'boolean') html.push(rowCheck(field.key, fid, field.value));
- else html.push(rowText(field.key, fid, field.value));
+ html.push(renderField(c.index, fields[f]));
}
html.push('
');
}
@@ -100,6 +87,35 @@
this._bindSections();
};
+ /**
+ * @param {number} compIndex
+ * @param {object} field
+ * @returns {string}
+ */
+ function renderField(compIndex, field) {
+ var label = field.label || field.key;
+ var fid = 'c' + compIndex + '_' + field.key;
+ var type = field.type;
+ if (field.readOnly || type === 'ref') {
+ return rowRef(label, field.value);
+ }
+ if (type === 'boolean') return rowCheck(label, fid, !!field.value);
+ if (type === 'number') return rowNum(label, fid, field.value);
+ if (type === 'string') return rowText(label, fid, field.value);
+ if (type === 'enum') return rowEnum(label, fid, field.value, field.enumOptions || []);
+ if (type === 'vec2') {
+ return rowVec2(label, [fid + '_x', fid + '_y'], [field.value.x, field.value.y], ['X', 'Y']);
+ }
+ if (type === 'vec3') {
+ return rowVec3(label, [fid + '_x', fid + '_y', fid + '_z'], [field.value.x, field.value.y, field.value.z]);
+ }
+ if (type === 'size') {
+ return rowVec2(label, [fid + '_w', fid + '_h'], [field.value.width, field.value.height], ['W', 'H']);
+ }
+ if (type === 'color') return rowColor(label, fid, field.value);
+ return rowText(label, fid, field.value == null ? '' : String(field.value));
+ }
+
InspectorPanel.prototype._bindSections = function () {
var heads = this.body.querySelectorAll('.pw-insp-sec-head[data-toggle]');
for (var i = 0; i < heads.length; i++) {
@@ -120,35 +136,89 @@
var run = function () {
var fid = el.getAttribute('data-fid');
var value;
- if (el.type === 'checkbox') value = !!el.checked;
+ if (el.tagName === 'SELECT') value = parseEnumValue(el);
+ else if (el.type === 'checkbox') value = !!el.checked;
+ else if (el.type === 'color') value = el.value;
else if (el.type === 'number') value = parseFloat(el.value);
else value = el.value;
self._bridge.call('node.set', { uuid: self._uuid, fid: fid, value: value });
+ if (el.getAttribute('data-color-pair')) {
+ var siblings = el.parentNode ? el.parentNode.querySelectorAll('[data-fid="' + fid + '"]') : [];
+ for (var si = 0; si < siblings.length; si++) {
+ if (siblings[si] === el) continue;
+ var nv = String(value);
+ if (nv.charAt(0) !== '#' && siblings[si].type === 'color') nv = '#' + nv;
+ if (siblings[si].type === 'color') siblings[si].value = nv.slice(0, 7);
+ else siblings[si].value = nv;
+ }
+ }
};
el.addEventListener('change', run);
- el.addEventListener('input', run);
+ if (el.type !== 'checkbox' && el.tagName !== 'SELECT') el.addEventListener('input', run);
})(inputs[i]);
}
};
+ function parseEnumValue(el) {
+ var opt = el.options[el.selectedIndex];
+ if (!opt) return el.value;
+ var raw = opt.getAttribute('data-num');
+ if (raw != null && raw !== '') return Number(raw);
+ return el.value;
+ }
+
+ function labelHtml(label) {
+ var text = label == null ? '' : String(label);
+ return '
' + escapeHtml(text) + ' ';
+ }
function rowText(label, id, value) {
- return '
' + escapeHtml(label) +
- ' ' + labelHtml(label) +
+ '
';
}
function rowNum(label, id, value) {
- return '
' + escapeHtml(label) +
- ' ' + labelHtml(label) +
+ '
';
}
function rowCheck(label, id, value) {
- return '
' + escapeHtml(label) +
- ' ' + labelHtml(label) +
+ '
';
}
+ function rowEnum(label, id, value, options) {
+ var opts = [];
+ for (var i = 0; i < options.length; i++) {
+ var o = options[i];
+ var selected = o.value === value || String(o.value) === String(value);
+ opts.push('
' +
+ escapeHtml(o.name) + ' ');
+ }
+ return '
' + labelHtml(label) +
+ '' + opts.join('') + '
';
+ }
+ function rowRef(label, value) {
+ var v = value || {};
+ var text = (v.kind ? v.kind + ' · ' : '') + (v.name || '(空)');
+ return '
' + labelHtml(label) +
+ '
' +
+ '' + escapeHtml(v.kind || 'Object') + ' ' +
+ '' + escapeHtml(v.name || '(空)') + '
';
+ }
+ function rowColor(label, id, value) {
+ var r = (value && value.r) || 0;
+ var g = (value && value.g) || 0;
+ var b = (value && value.b) || 0;
+ var hex = '#' + toHex2(r) + toHex2(g) + toHex2(b);
+ return '
';
+ }
function rowVec3(label, ids, values) {
- return '
' +
- '
' + escapeHtml(label) + ' ' +
+ return '
' + labelHtml(label) +
'
' +
axis('X', ids[0], values[0]) +
axis('Y', ids[1], values[1]) +
@@ -156,8 +226,7 @@
'
';
}
function rowVec2(label, ids, values, axisNames) {
- return '
' +
- '
' + escapeHtml(label) + ' ' +
+ return '
' + labelHtml(label) +
'
' +
axis(axisNames[0], ids[0], values[0]) +
axis(axisNames[1], ids[1], values[1]) +
@@ -168,6 +237,10 @@
'
';
}
+ function toHex2(n) {
+ var s = Math.max(0, Math.min(255, n | 0)).toString(16);
+ return s.length < 2 ? '0' + s : s;
+ }
function escapeHtml(s) {
return String(s).replace(/&/g, '&').replace(//g, '>');