修运行画面点选后预览自己缩放:高亮框不再参与尺寸上报,命中矩形裁剪到画布
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

- 根因:高亮框(body 下 data-genarrative-preview-inspect,position:fixed)被尺寸上报的 measureContentBounds 遍历计入内容尺寸;three 投影盒超出画布时把内容尺寸撑到 3756×2643(视口 900×560),宿主改 iframe 尺寸触发游戏 resize 重算相机,形成自激振荡
- 桥:measureContentBounds 的 TreeWalker 对 data-genarrative-preview-fit / data-genarrative-preview-inspect 返回 FILTER_REJECT(桥自己的节点不参与内容测量)
- 桥:新增 clipInspectRect,引擎档(three 投影盒、Phaser 世界包围盒)命中矩形统一裁剪到画布可见范围;与画布无交集时退化为指针点矩形(不出现 0/负尺寸)
- 测试:新增 localPreviewInspectSizeStability.test.ts(5 例:桥节点不参与测量、同尺寸普通节点仍计入的对照组、进入检查模式与 hover 后尺寸与宿主适配布局不变、投影盒超出画布时载荷被裁剪、与画布无交集时退化为 1×1);runtimeInspectEngines 的字段换算用例改到画布内的等价参数(原矩形落在画布外,裁剪后按新规则退化为指针点矩形)
- 文档:pitfalls 记一条「高亮框被尺寸上报当成页面内容」的根因、口径与回归
This commit is contained in:
2026-10-05 17:08:03 +08:00
parent 8d1980488e
commit 7ad412f0ec
4 changed files with 569 additions and 7 deletions
@@ -170,9 +170,24 @@
heightBaseline: height.baseline,
};
};
// 桥自己的节点(尺寸上报的 style、点选高亮框)不属于页面内容:高亮框还是 `position: fixed`
// 且跟随命中对象,一旦被 `measureContentBounds` 当成固有尺寸,宿主就会据此改 iframe 尺寸、
// 游戏 resize 重算相机——实测表现为「点选后画面自己缩放」。
const isPreviewBridgeNode = (element) => {
const dataset = element && element.dataset;
if (!dataset) return false;
return (
dataset.genarrativePreviewFit !== undefined
|| dataset.genarrativePreviewInspect !== undefined
);
};
const measureContentBounds = (body, viewportWidth, viewportHeight) => {
if (!body) return { width: 0, height: 0, truncated: false };
const walker = document.createTreeWalker(body, NodeFilter.SHOW_ELEMENT);
const walker = document.createTreeWalker(body, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => (
isPreviewBridgeNode(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
),
});
let width = 0;
let height = 0;
let widthBaseline = 0;
@@ -499,6 +514,19 @@
width: 1,
height: 1,
});
// 命中矩形裁剪到画布可见范围:引擎侧的世界 / 投影包围盒可能远超画布,高亮框与载荷都不该出现
// 比画面还大的矩形(同时它也是「点选后画面自己缩放」的输入之一,见 measureContentBounds 注释)。
// 与画布无交集时返回 null,由调用方退化成指针点矩形。
const clipInspectRect = (rect, canvasRect) => {
if (!rect || !canvasRect) return null;
const left = Math.max(rect.left, canvasRect.left);
const top = Math.max(rect.top, canvasRect.top);
const right = Math.min(rect.left + rect.width, canvasRect.left + canvasRect.width);
const bottom = Math.min(rect.top + rect.height, canvasRect.top + canvasRect.height);
if (![left, top, right, bottom].every(Number.isFinite)) return null;
if (right <= left || bottom <= top) return null;
return { left, top, width: Math.max(1, right - left), height: Math.max(1, bottom - top) };
};
const engineInspectSelection = (identity, rect) => ({
label: identity.label || 'canvas',
elementTag: 'canvas',
@@ -835,7 +863,8 @@
} catch {
boundsRect = null;
}
const rect = boundsRect || inspectPointerRect(clientX, clientY);
const canvasRect = canvasElement.getBoundingClientRect();
const rect = clipInspectRect(boundsRect, canvasRect) || inspectPointerRect(clientX, clientY);
return { selection: engineInspectSelection(phaserResourceIdentity(gameObject), rect), rect };
};
const phaserInspectTarget = (handle, canvasElement, clientX, clientY) => {
@@ -1016,7 +1045,8 @@
const hit = Array.isArray(hits) && hits.length > 0 ? hits[0] : null;
const object = hit && hit.object;
if (!object) return null;
const rect = threeBoxScreenRect(three, object, camera, domRect) || inspectPointerRect(clientX, clientY);
const rect = clipInspectRect(threeBoxScreenRect(three, object, camera, domRect), domRect)
|| inspectPointerRect(clientX, clientY);
return { selection: engineInspectSelection(threeResourceIdentity(object), rect), rect };
};
const resolveInspectEngineSelection = (canvasElement, clientX, clientY) => {
File diff suppressed because it is too large Load Diff
@@ -710,8 +710,8 @@ describe('运行画面点选:Phaser 4 档', () => {
data: { genarrativeResourceId: 'local-asset:plain' },
});
const camera = createPhaserCamera({
scrollX: 10,
scrollY: 20,
scrollX: -400,
scrollY: -300,
zoom: 2,
x: 3,
y: 4,
@@ -733,8 +733,9 @@ describe('运行画面点选:Phaser 4 档', () => {
sendInspectAction('enable');
clickAt(canvas, 50, 40);
// 世界 (10,20)-(110,70) 经 origin=0.5 / zoom=2 / scroll=(10,20) / camera=(3,4)
// → 屏幕 (-397,-296)-( -197,-196):宽 200、高 100。
// 相机字段换算(origin=0.5 / zoom=2 / scroll=(-400,-300) / camera=(3,4)):
// 世界 (10,20)-(110,70) → 屏幕 (423,344)-(623,444),宽 200、高 100(世界尺寸的 2 倍)。
// 矩形落在画布内,画布裁剪不改变它。
expect(selectedSelection()).toMatchObject({
width: 200,
height: 100,
@@ -2,6 +2,14 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 2026-10-05 运行画面点选的高亮框被尺寸上报当成页面内容:点选后预览自己缩放
- **现象**:three 项目里点选 3D 对象后预览画面自己缩放、视角异常(相机 aspect 与重新适配的距离都变了),点选结束后视口仍停在放大后的尺寸。实测夹具里是**自激振荡**:10 秒内 1459 条尺寸消息、1457 次 iframe 尺寸/缩放变更、1454 次 `resize`,相机 aspect 在 1.607143 ↔ 1.421112 之间来回变。
- **原因**:高亮框是 `document.body` 下的 `div[data-genarrative-preview-inspect]`(`position: fixed`、尺寸随命中对象),而尺寸上报的 `measureContentBounds` 用 `createTreeWalker(body, SHOW_ELEMENT)` 遍历所有元素并把 `rect.right/bottom` 计入内容尺寸。three 档的高亮矩形来自 `Box3` 八角投影,物体贴近相机时投影盒远超画布(实测 6607×4721、left/top 为负)→ 上报内容被撑到 3756×2643(视口只有 900×560)→ 宿主 `resolveLocalGamePreviewFitLayout` 把 iframe 改成 3756×2643 + scale 0.2119 → 游戏 `resize` 重算相机 → 视口变大又让投影盒更大,如此循环。
- **结论(现行口径)**:① 桥自己的节点一律不进内容尺寸测量——`measureContentBounds` 的 TreeWalker 用 `acceptNode` 对 `data-genarrative-preview-fit` / `data-genarrative-preview-inspect` 返回 `FILTER_REJECT`;以后新增任何桥注入的 DOM 节点都要带上这两个标记之一,否则会重新引入这条反馈。② 引擎档命中矩形统一裁剪到画布可见范围(`clipInspectRect`;与画布无交集时退化为指针点矩形),不再出现比画面还大的高亮框。
- **回归**:`apps/ai-game-creator-shell/tests/localPreviewInspectSizeStability.test.ts`(5 例:桥节点不参与测量、同尺寸普通节点仍计入的对照组、进入检查模式与 hover 后尺寸与宿主适配布局不变、投影盒超出画布时载荷被裁剪、与画布无交集时退化为 1×1)。
- **关联**:`resources/preview/local-preview-fit.js`(`measureContentBounds` / `clipInspectRect` / `threeInspectTarget` / `phaserSelection`)、`features/project-workspace/LocalGamePreviewFrame.tsx`(`resolveLocalGamePreviewFitLayout`)。
## 2026-10-05 把整个 `THREE` 命名空间塞进预览句柄会让 tree-shaking 失效
- **现象**:three 项目的运行画面点选句柄写成 `{ engine: 'three', THREE, scene, camera, renderer }` 时,同一个 Vite 构建的产物从 517,682 B 涨到 730,831 B(+213,149 B ≈ +41%)。