运行画面点选:three 句柄瘦身 + 命中对象 label 兜底
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

- 桥:three 句柄改读 `handle.three || handle.THREE`(旧整命名空间形态仍兼容),四个构造器缺任一即按未命中退化
- 桥:three label 链补 `object.name` → `geometry.type` → `object.type`,Phaser 补 `name` → `type`(取值超 40 字符丢弃);命中对象但取不到素材身份时芯片不再一律是 `@canvas`
- 契约:agc-web-game-development 的「运行画面点选契约」改用瘦身句柄 `three: { Raycaster, Vector2, Vector3, Box3 }`,并写明整命名空间会破坏 tree-shaking(同一 Vite 构建 517,682 B → 730,831 B,+41%);skill-pack 指纹同步到 2026-08-26.39
- 模板:threejs-3d-starter 与 blank-3d-scene 的句柄源改成瘦身形态
- 测试:runtimeInspectEngines 新增 6 例(瘦身句柄命中、旧命名空间兼容、name / geometry.type / object.type 兜底、缺构造器退化),共 33 例
- 文档:功能说明补 label 兜底与瘦身句柄的验收行和边界;pitfalls 记一条 tree-shaking 口径;实施计划阶段二进展补该轮结论
This commit is contained in:
2026-10-05 15:23:46 +08:00
parent 7d205c8da0
commit 19d179bde4
10 changed files with 299 additions and 13 deletions
@@ -33,7 +33,10 @@ When implementing a new game loop or a broad gameplay revision, read `references
1. **发布只读引擎句柄**:引擎实例创建后发布一次,只服务点选与调试,不承载业务状态。句柄缺失或 `engine` 不是 `phaser` / `three` 时点选按第 4 条退化。
- Phaser:`window.__GENARRATIVE_PREVIEW_GAME__ = { engine: 'phaser', game };`
- three.js:`window.__GENARRATIVE_PREVIEW_GAME__ = { engine: 'three', THREE, scene, camera, renderer };`(`THREE` 是 `import * as THREE from 'three'` 的命名空间,点选要靠它做射线与包围盒)
- three.js:`window.__GENARRATIVE_PREVIEW_GAME__ = { engine: 'three', three: { Raycaster, Vector2, Vector3, Box3 }, scene, camera, renderer };`
- **不要传整个 `three` 命名空间**:实测同一个 Vite 构建因此从 517,682 B 涨到 730,831 B(+213,149 B ≈ +41%),tree-shaking 失效。
- 这四个构造器是预览桥做射线与包围盒投影的最小集;该字段只给预览桥用,玩法代码不要引用它。
- 旧形态 `{ engine: 'three', THREE, scene, camera, renderer }` 仍被兼容读取,新代码一律用上面的瘦身形态。
2. **Phaser 图片用 Image 标签装载**:游戏配置固定 `loader: { imageLoadType: 'HTMLImageElement' }`。默认的 XHR 装载把贴图地址换成会话内临时地址,点选读不回原始素材路径;这类项目只能靠第 3 条的显式标注。
3. **给渲染对象标注素材身份**:用已登记素材渲染的对象要带上 manifest 的稳定身份,值只取 `agc_list_registered_assets` 返回的 `localAssetId` / `localPath`,不自行编造、不改写文件名。
- Phaser:`sprite.setData('genarrativeResourceId', '<localAssetId>')`,或 `sprite.setData('genarrativeResourcePath', '<localPath>')`;两者都给时 id 优先。
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.38",
"version": "2026-08-26.39",
"skills": [
{
"name": "agc-unity-editor",
@@ -121,7 +121,7 @@
"agents/openai.yaml",
"references/game-quality-checklist.md"
],
"sha256": "55fb4e14fba3682028b4508ac1721a907d1196cf7527113ebb4977d6025d0cdc"
"sha256": "d92394d7bfc660fa436c0753822c8f2da12c57b04e091bbf065f471ce27d6cc9"
},
{
"name": "agc-browser-playtest",
@@ -478,7 +478,7 @@
return '';
}
};
// 标签链:显式路径文件名去扩展名 → 贴图 URL 同样派生 → Phaser texture.key → 'canvas'。
// 标签链:显式路径文件名去扩展名 → 贴图 URL 同样派生 → 引擎侧对象名/类型 → 'canvas'。
const inspectLabelFromSource = (value) => {
const trimmed = inspectTextValue(value);
if (!trimmed) return '';
@@ -488,6 +488,11 @@
const stem = dot > 0 ? segment.slice(0, dot) : (dot === 0 ? '' : segment);
return stem.trim();
};
// 对象名 / 对象类型兜底标签:超过 40 字符一律丢掉,避免把程序化命名塞进引用芯片。
const inspectObjectLabel = (value) => {
const text = inspectTextValue(value);
return text.length > 40 ? '' : text;
};
const inspectPointerRect = (clientX, clientY) => ({
left: Number.isFinite(clientX) ? clientX : 0,
top: Number.isFinite(clientY) ? clientY : 0,
@@ -720,6 +725,12 @@
if (!identity.label) {
identity.label = inspectTextValue(gameObject && gameObject.texture && gameObject.texture.key);
}
if (!identity.label) {
identity.label = inspectObjectLabel(gameObject && gameObject.name);
}
if (!identity.label) {
identity.label = inspectObjectLabel(gameObject && gameObject.type);
}
return identity;
};
// Phaser 4 的权威换算是 `camera.matrixCombined`(matrix × matrixExternal,Camera.preRender 组装),
@@ -914,6 +925,16 @@
identity.label = inspectLabelFromSource(textureUrl);
}
}
// 没有素材身份时仍给一个画面侧可得的名字:对象名 → 几何类型(BoxGeometry 等)→ 对象类型(Mesh 等)。
if (!identity.label) {
identity.label = inspectObjectLabel(object && object.name);
}
if (!identity.label) {
identity.label = inspectObjectLabel(object && object.geometry && object.geometry.type);
}
if (!identity.label) {
identity.label = inspectObjectLabel(object && object.type);
}
return identity;
};
// 命中对象世界包围盒八角投影取 min/max;取不到时不阻塞,退化成指针点矩形。
@@ -963,14 +984,22 @@
}
};
const threeInspectTarget = (handle, clientX, clientY) => {
const three = handle.THREE;
// `three` 是推荐形态(只放桥要用的四个构造器);`THREE` 是整命名空间旧形态,保留兼容。
const three = handle.three || handle.THREE;
const scene = handle.scene;
const camera = handle.camera;
const renderer = handle.renderer;
const domElement = renderer && renderer.domElement;
if (!three || !scene || !camera || !domElement) return null;
if (typeof domElement.getBoundingClientRect !== 'function') return null;
if (typeof three.Vector2 !== 'function' || typeof three.Raycaster !== 'function') return null;
if (
typeof three.Raycaster !== 'function'
|| typeof three.Vector2 !== 'function'
|| typeof three.Vector3 !== 'function'
|| typeof three.Box3 !== 'function'
) {
return null;
}
const domRect = domElement.getBoundingClientRect();
const width = Number(domRect && domRect.width);
const height = Number(domRect && domRect.height);
@@ -19,9 +19,15 @@ renderer.setSize(window.innerWidth, window.innerHeight);
container.append(renderer.domElement);
// 发布运行画面实例句柄,供预览桥点选定位场景、相机与渲染器。
// three 字段只放预览桥要用的构造器:整命名空间会破坏 tree-shaking(实测包体 +41%)。
window.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
THREE,
three: {
Raycaster: THREE.Raycaster,
Vector2: THREE.Vector2,
Vector3: THREE.Vector3,
Box3: THREE.Box3,
},
scene,
camera,
renderer,
@@ -19,9 +19,15 @@ renderer.setSize(window.innerWidth, window.innerHeight);
container.append(renderer.domElement);
// 发布运行画面实例句柄,供预览桥点选定位场景、相机与渲染器。
// three 字段只放预览桥要用的构造器:整命名空间会破坏 tree-shaking(实测包体 +41%)。
window.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
THREE,
three: {
Raycaster: THREE.Raycaster,
Vector2: THREE.Vector2,
Vector3: THREE.Vector3,
Box3: THREE.Box3,
},
scene,
camera,
renderer,
@@ -59,6 +59,8 @@ type FakeSpriteTexture = {
type FakeSprite = {
visible: boolean;
depth: number;
name: string;
type: string;
texture: FakeSpriteTexture;
getBounds: () => FakeBounds;
getData: (key: string) => string | undefined;
@@ -125,6 +127,8 @@ function createSprite(options: {
textureSource?: { currentSrc?: string; src?: string };
depth?: number;
visible?: boolean;
name?: string;
objectType?: string;
}): FakeSprite {
const texture: FakeSpriteTexture = { key: options.textureKey ?? '' };
const sourceImage = options.sourceImage;
@@ -134,6 +138,8 @@ function createSprite(options: {
const sprite: FakeSprite = {
visible: options.visible ?? true,
depth: options.depth ?? 0,
name: options.name ?? '',
type: options.objectType ?? '',
texture,
getBounds: () => ({
x: options.bounds.left,
@@ -304,8 +310,15 @@ function createThreeMesh(options: {
userData?: Record<string, unknown>;
material?: unknown;
box?: StubRect;
name?: string;
geometryType?: string;
objectType?: string;
}) {
const mesh = {
name: options.name ?? '',
type: options.objectType ?? '',
geometry:
options.geometryType === undefined ? {} : { type: options.geometryType },
userData: options.userData ?? {},
material: options.material ?? {},
};
@@ -860,6 +873,47 @@ describe('运行画面点选:Phaser 4 档', () => {
expect(degraded?.style.width).toBe('1px');
expect(degraded?.style.height).toBe('1px');
});
it('对象没有贴图时标签取对象名 / 对象类型,不再一律落到 canvas', () => {
const canvas = createCanvas({ left: 0, top: 0, width: 800, height: 600 });
const named = createSprite({
bounds: { left: 0, top: 0, width: 100, height: 100 },
name: 'Hero',
objectType: 'Sprite',
});
const scene = createPhaserScene({ active: true, interactiveHits: [named] });
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'phaser',
game: createPhaserGame({
canvas,
pointer: { x: 50, y: 50 },
scenes: [scene],
}),
};
sendInspectAction('enable');
clickAt(canvas, 50, 50);
expect(selectedSelection()).toMatchObject({
label: 'Hero',
resourceIds: [],
sourcePath: '',
});
// 没有对象名时退到对象类型。
const unnamed = createSprite({
bounds: { left: 0, top: 0, width: 100, height: 100 },
objectType: 'Sprite',
});
scene.input.hitTestPointer = () => [unnamed];
sendInspectAction('enable');
posted.length = 0;
clickAt(canvas, 50, 50);
expect(selectedSelection()).toMatchObject({
label: 'Sprite',
resourceIds: [],
sourcePath: '',
});
});
});
describe('运行画面点选:three.js 档', () => {
@@ -1071,6 +1125,184 @@ describe('运行画面点选:three.js 档', () => {
resourceIds: [],
});
});
it('瘦身句柄(three 只放四个构造器)可命中贴图 Mesh', () => {
const canvas = createCanvas({ left: 0, top: 0, width: 800, height: 600 });
const mesh = createThreeMesh({
material: {
map: { image: { src: 'http://localhost:3000/assets/slim-hero.png' } },
},
box: { left: 100, top: 50, width: 200, height: 100 },
});
const three = createThreeNamespace({
hits: [{ object: mesh, distance: 1 }],
projection,
});
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
three,
scene: { children: [mesh] },
camera: {},
renderer: { domElement: canvas },
};
sendInspectAction('enable');
clickAt(canvas, 200, 200);
expect(selectedSelection()).toEqual({
label: 'slim-hero',
elementTag: 'canvas',
elementRole: '',
text: '',
width: 200,
height: 100,
resourceIds: [],
sourcePath: '/assets/slim-hero.png',
});
});
it('旧形态(整个 THREE 命名空间)仍兼容命中', () => {
const canvas = createCanvas({ left: 0, top: 0, width: 800, height: 600 });
const mesh = createThreeMesh({
userData: { genarrativeResourceId: 'local-asset:legacy' },
box: { left: 0, top: 0, width: 100, height: 100 },
});
const three = createThreeNamespace({
hits: [{ object: mesh, distance: 1 }],
projection,
});
// 旧句柄把整个 `import * as THREE` 命名空间塞进来:额外成员不影响命中。
const namespace = {
...three,
Scene: function Scene() {},
Color: function Color() {},
WebGLRenderer: function WebGLRenderer() {},
};
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
THREE: namespace,
scene: { children: [mesh] },
camera: {},
renderer: { domElement: canvas },
};
sendInspectAction('enable');
clickAt(canvas, 10, 10);
expect(selectedSelection()).toMatchObject({
label: 'canvas',
resourceIds: ['local-asset:legacy'],
sourcePath: '',
});
});
it('命中对象没有素材身份时标签取 object.name', () => {
const canvas = createCanvas({ left: 0, top: 0, width: 800, height: 600 });
const mesh = createThreeMesh({
name: 'Crate',
geometryType: 'BoxGeometry',
objectType: 'Mesh',
box: { left: 0, top: 0, width: 100, height: 100 },
});
const three = createThreeNamespace({
hits: [{ object: mesh, distance: 1 }],
projection,
});
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
three,
scene: { children: [mesh] },
camera: {},
renderer: { domElement: canvas },
};
sendInspectAction('enable');
clickAt(canvas, 50, 50);
// 没有任何素材身份(无 userData、材质无贴图):标签不再一律是 canvas。
expect(selectedSelection()).toMatchObject({
label: 'Crate',
resourceIds: [],
sourcePath: '',
});
expect(sanitizedSelection()).toMatchObject({
label: 'Crate',
resourceIds: [],
});
expect(sanitizedSelection().sourcePath).toBeUndefined();
});
it('没有对象名时标签取 geometry.type,几何也取不到时取 object.type', () => {
const canvas = createCanvas({ left: 0, top: 0, width: 800, height: 600 });
const sphere = createThreeMesh({
geometryType: 'SphereGeometry',
objectType: 'Mesh',
box: { left: 0, top: 0, width: 100, height: 100 },
});
const three = createThreeNamespace({
hits: [{ object: sphere, distance: 1 }],
projection,
});
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
three,
scene: { children: [sphere] },
camera: {},
renderer: { domElement: canvas },
};
sendInspectAction('enable');
clickAt(canvas, 50, 50);
expect(selectedSelection()).toMatchObject({ label: 'SphereGeometry' });
const group = createThreeMesh({
objectType: 'Group',
box: { left: 0, top: 0, width: 100, height: 100 },
});
const second = createThreeNamespace({
hits: [{ object: group, distance: 1 }],
projection,
});
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
three: second,
scene: { children: [group] },
camera: {},
renderer: { domElement: canvas },
};
sendInspectAction('enable');
posted.length = 0;
clickAt(canvas, 50, 50);
expect(selectedSelection()).toMatchObject({ label: 'Group' });
});
it('句柄缺 Box3 / Vector3 等构造器时按未命中退化', () => {
const canvas = createCanvas({ left: 0, top: 0, width: 800, height: 600 });
const mesh = createThreeMesh({
userData: { genarrativeResourceId: 'local-asset:incomplete' },
box: { left: 0, top: 0, width: 100, height: 100 },
});
const three = createThreeNamespace({
hits: [{ object: mesh, distance: 1 }],
projection,
});
bridgeWindow.__GENARRATIVE_PREVIEW_GAME__ = {
engine: 'three',
three: { Raycaster: three.Raycaster, Vector2: three.Vector2 },
scene: { children: [mesh] },
camera: {},
renderer: { domElement: canvas },
};
sendInspectAction('enable');
clickAt(canvas, 50, 50);
expect(selectedSelection()).toMatchObject({
label: 'canvas',
resourceIds: [],
sourcePath: '',
});
});
});
describe('运行画面点选:退化路径', () => {
@@ -67,8 +67,10 @@
阶段一让点选能识别素材,但可用性依赖玩法满足三条约定(发布句柄、Phaser 用 Image 装载、对象标注素材身份);阶段二把这几条写进**生成期契约**,新生成的 Phaser 4 / three.js 项目不改代码即可点出素材。同一 PR 续推,`template.zip` 与线上索引仍未重新发布。
- 主源(唯一写全契约处):`resources/agc-skills/agc-web-game-development/SKILL.md` 新增「运行画面点选契约(运行态素材身份)」四条——发布只读引擎句柄(`{ engine:'phaser', game }` / `{ engine:'three', THREE, scene, camera, renderer }`)、Phaser 固定 `loader: { imageLoadType: 'HTMLImageElement' }`、给素材渲染对象标注 manifest 身份(Phaser `setData('genarrativeResourceId' | 'genarrativeResourcePath', …)`,three.js `mesh.userData.…`,值只取 `agc_list_registered_assets` 的 `localAssetId` / `localPath`)、素材进 dist 走与 `localPath` 对齐的稳定路径(`public/` 原样拷贝,不用 `import` 的 hash 产物名);并写明不满足时不报错、不伪造 id、退化为不带素材的区域引用(= 验收面)。
- 主源(唯一写全契约处):`resources/agc-skills/agc-web-game-development/SKILL.md` 新增「运行画面点选契约(运行态素材身份)」四条——发布只读引擎句柄(`{ engine:'phaser', game }` / `{ engine:'three', three: { Raycaster, Vector2, Vector3, Box3 }, scene, camera, renderer }`,three 瘦身形态见下条)、Phaser 固定 `loader: { imageLoadType: 'HTMLImageElement' }`、给素材渲染对象标注 manifest 身份(Phaser `setData('genarrativeResourceId' | 'genarrativeResourcePath', …)`,three.js `mesh.userData.…`,值只取 `agc_list_registered_assets` 的 `localAssetId` / `localPath`)、素材进 dist 走与 `localPath` 对齐的稳定路径(`public/` 原样拷贝,不用 `import` 的 hash 产物名);并写明不满足时不报错、不伪造 id、退化为不带素材的区域引用(= 验收面)。
- 派生副本只补指针,不复制契约:`agc-web-game-development/references/game-quality-checklist.md` 增加一条勾选项;`agc-game-production-workflow/SKILL.md` 第 4 步(Game implementation)指向该契约(新游戏编排必读)。
- 指纹同步:`npm run agc:skill-pack:sync` 更新 `resources/agc-skills/manifest.json` 的 `version` 与两个 skill 的 `sha256`;`skill_pack.rs` 逐文件 `include_bytes!`,因此**未新增 skill 文件、未改任何 Rust**。
- 验证:`npm run agc:typecheck`(含 skill-pack 指纹校验)、`node scripts/check-doc-index.mjs`、`npm run check:encoding`、`git diff --check` 全部通过(退出码与结论行见 PR 评论)。
- 冻结面不变:`preview.rs`、`resources/preview/local-preview-fit.js`(sha256 仍为 `6276ac48…917a`)、宿主匹配、脚手架与模板源均未改;未合并主线。
- 可见性补强 + 句柄瘦身:桥端 three 句柄改推荐瘦身形态 `three: { Raycaster, Vector2, Vector3, Box3 }`(旧 `THREE` 命名空间仍兼容读取,缺任一构造器按未命中退化),并补 label 兜底——three `object.name` → `geometry.type` → `object.type`、Phaser `texture.key` → `name` → `type`,命中对象但取不到素材身份时芯片不再一律是 `@canvas`;契约文本、`threejs-3d-starter` 与 `blank-3d-scene` 模板同步瘦身(整命名空间实测把 517,682 B 的构建撑到 730,831 B,+41%)。
- 该轮验证:`tests/runtimeInspectEngines.test.ts` 33 例(新增 6 例:瘦身句柄、旧命名空间兼容、name / geometry.type / object.type 兜底、缺构造器退化);真实 Chromium + 真实 three 0.184 + 真实桥脚本实测 slim 句柄命中、`Crate` / `SphereGeometry` / `LegacyMesh` 三种芯片名与对象区域矩形;`agc:typecheck`、`check-doc-index`、`check:encoding`、`git diff --check` 通过。
@@ -40,8 +40,8 @@
- [x] Phaser 4 起步工程:点中由已登记素材渲染的精灵 / 图集帧,引用芯片显示该素材名,载荷 `resourceIds` 命中该素材 id。(浏览器实测:`loader.imageLoadType='HTMLImageElement'` 下自动档 `@hero` + `["hero"]`;默认 XHR 装载贴图为 blob,须由玩法显式标注,见主规范边界)
- [x] three.js 起步工程:点中贴了已登记贴图的 Mesh(含 GLB 内嵌纹理的模型走显式标注路径),结果同上。(浏览器实测:贴图 Mesh `@hero` + `["hero"]`;`userData.genarrativeResourcePath` 档 `@enemy` + `["enemy-tag"]`)
- [x] 同一对象同时存在显式素材标注与贴图地址推断时,取显式标注。(`tests/runtimeInspectEngines.test.ts` 27 例;浏览器重叠位取最上层且显式 id 胜出 `["enemy-tag"]`)
- [x] 无引擎句柄 / 点中空白 / 程序化绘制三种情况:退化为不带素材的区域引用,零报错、零伪造 id,引用可插入可发送。(`runtimeInspectEngines.test.ts` 27 例;浏览器点空白 `@canvas` + `[]` 零报错)
- [x] 同一对象同时存在显式素材标注与贴图地址推断时,取显式标注。(`tests/runtimeInspectEngines.test.ts` 33 例;浏览器重叠位取最上层且显式 id 胜出 `["enemy-tag"]`)
- [x] 无引擎句柄 / 点中空白 / 程序化绘制三种情况:退化为不带素材的区域引用,零报错、零伪造 id,引用可插入可发送。(`runtimeInspectEngines.test.ts` 33 例;浏览器点空白 `@canvas` + `[]` 零报错)
- [x] 命中对象时高亮为该对象在画面上的区域;取不到对象矩形时高亮范围与 DOM 档一致。(矩形换算改用 `camera.matrixCombined`;浏览器实测与期望逐值相减 Phaser 差 0px、three 最大差 ≈0.0006px,DOM 档为元素矩形)
- [x] DOM 点选(HTML HUD、媒体 `src`、资源标识属性)行为与现有用例逐字不变。(`localGamePreviewFrame` 14、`resourceReferenceInput` 43、`appSurface` 197 passed / 9 skipped;浏览器实测 HUD 芯片名 `@HUD 区域` 未被素材名改写)
- [x] 起步工程与新建项目默认脚手架发布引擎句柄;新建 Phaser 4 项目不额外改代码即可点选。(`DEFAULT_GAME_SCRIPT_JS` 与三个起步模板源已发布句柄;`cargo test -- scaffold` 4 passed / 1 既有 ignore。开箱即用未跑真机链路)
@@ -55,7 +55,7 @@
## 验收证据(2026-10-04)
- 命令:`npm run agc:typecheck` 通过(`check:tests:types` / `tsc` / skill-pack 指纹 / 包布局声明 / `check-config` 全过)。
- 前端用例:`runtimeInspectEngines.test.ts`(27)+ `runtimeInspectResourceMatch.test.ts`(11)+ `localGamePreviewFrame.test.ts`(14)+ `resourceReferenceInput.test.tsx`(43)= 95 passed;`appSurface.test.ts` 197 passed / 9 skipped。
- 前端用例:`runtimeInspectEngines.test.ts`(33)+ `runtimeInspectResourceMatch.test.ts`(11)+ `localGamePreviewFrame.test.ts`(14)+ `resourceReferenceInput.test.tsx`(43)= 101 passed;`appSurface.test.ts` 197 passed / 9 skipped。
- Rust:`cargo test … -- preview` 43 passed / 0 failed;`cargo test … -- scaffold` 4 passed / 1 既有 `#[ignore]`(需 npm 装依赖 + Provider)。
- 真实浏览器冒烟(真实 Phaser 4.2.1 / three 0.184 + 真实桥脚本 `local-preview-fit.js`,被测 sha256 `6276ac48…917a`):已登记贴图对象得 `["hero"]`、显式标注对象得 `["enemy-tag"]`、非交互精灵经几何兜底命中;点空白 `@canvas` + `[]` 零报错;DOM 档芯片名不被改写;高亮矩形与期望逐值相减,Phaser 差 0px、three 最大差 ≈0.0006px。
- 未覆盖:Tauri 真机链路(预览服务器注入与路由由 Rust 定向测试覆盖)、多场景叠加、非默认相机(scroll/zoom/rotation)、高亮真机目视。
@@ -2,6 +2,12 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 2026-10-05 把整个 `THREE` 命名空间塞进预览句柄会让 tree-shaking 失效
- **现象**:three 项目的运行画面点选句柄写成 `{ engine: 'three', THREE, scene, camera, renderer }` 时,同一个 Vite 构建的产物从 517,682 B 涨到 730,831 B(+213,149 B ≈ +41%)。
- **原因**:句柄引用整个 `import * as THREE` 命名空间,打包器无法证明未使用的导出可以剪掉;预览桥实际只用 `Raycaster` / `Vector2` / `Vector3` / `Box3` 四个构造器。
- **结论(现行口径)**:句柄用瘦身形态 `three: { Raycaster, Vector2, Vector3, Box3 }`;该字段只给预览桥用,玩法代码不要引用它。旧形态仍被兼容读取(两种形态都有用例锁定),但新代码与模板一律用瘦身形态。
## 2026-10-04 Phaser 4 `hitTestPointer` 的返回顺序不是叠放顺序
- **现象**:运行画面点选 Phaser 4 画面时,按「数组第一个 / 最后一个」当最上层会点错对象——点的是上层精灵,引用却落到下层的素材上;对象越多越容易错。
@@ -75,6 +75,7 @@ AGC 聊天输入框支持以结构化引用标记当前项目已登记素材,
- **Phaser 4 默认装载下贴图地址不可得**:Phaser 4 默认用 XHR 装载图片,贴图元素上的地址是会话内的临时地址,读不回原始素材路径。这类项目**必须由玩法显式标注素材身份**才能点出素材;没有标注就按第 4 条退化(退化为不带素材的区域引用),不猜。
- **只认玩法发布的只读引擎句柄**:预览页没有发布该句柄、或句柄声明的引擎类型不在支持范围内时,一律退化为不带素材的区域引用;宿主不通过扫描全局对象去猜引擎。
- **生成期契约落在 AGC 玩法 Skill**:句柄发布、Phaser 图片的 Image 装载、素材渲染对象的身份标注、素材在 dist 的稳定路径这四条写进 `agc-web-game-development` 的「运行画面点选契约」,生成期即按契约产出;本能力说明不承诺历史项目自动具备,也不改写既有项目。
- **three 句柄只放桥要用的构造器**:契约推荐 `{ engine: 'three', three: { Raycaster, Vector2, Vector3, Box3 }, scene, camera, renderer }`;传整个 `THREE` 命名空间会破坏 tree-shaking(同一 Vite 构建实测 517,682 B → 730,831 B,+41%)。旧形态仍被兼容读取,但新代码与模板只用瘦身形态。
### 验收标准与证据
@@ -82,8 +83,9 @@ AGC 聊天输入框支持以结构化引用标记当前项目已登记素材,
| ---- | -------- | ---- |
| 2、3 | Phaser 4 与 three.js 各一条真机 smoke:起步工程 + 一张已登记素材,点中渲染对象后核对引用芯片名与 `resourceIds` | 真实 Chromium 端到端(真实 Phaser 4.2.1 / three 0.184;Phaser 分别用 `loader.imageLoadType='HTMLImageElement'` 与默认 XHR 两种装载):已登记贴图对象得 `resourceIds=["hero"]`、显式标注对象得 `["enemy-tag"]`、非交互精灵走几何兜底也命中;被测桥脚本 sha256 `6276ac48…917a`。**Tauri 客户端链路未跑**(预览服务器注入与路由由 Rust 定向测试覆盖)。 |
| 1、6 | 现有 DOM 点选与运行视窗用例不回归 | `tests/localGamePreviewFrame.test.ts`(14 例)、`tests/resourceReferenceInput.test.tsx`(43 例)、`tests/appSurface.test.ts`(197 passed / 9 skipped)不回归;点选载荷仍走同一套净化与消息版本号。 |
| 4 | 无引擎句柄 / 命中空白 / 程序化绘制三种情况的退化用例 | `tests/runtimeInspectEngines.test.ts`(27 例)覆盖无句柄 / 点空白 / 缺 `renderer.domElement` 三种退化;浏览器点空白得 `@canvas` 且 `resourceIds=[]`、零报错。 |
| 4 | 无引擎句柄 / 命中空白 / 程序化绘制三种情况的退化用例 | `tests/runtimeInspectEngines.test.ts`(33 例)覆盖无句柄 / 点空白 / 缺 `renderer.domElement` 三种退化;浏览器点空白得 `@canvas` 且 `resourceIds=[]`、零报错。 |
| 5 | 高亮矩形:声明级用例 + 真机目视 | 浏览器实测高亮矩形与期望逐值相减:Phaser 差 0px、three 最大差 ≈0.0006px;DOM 档仍是元素矩形。 |
| 4、5 | 命中对象但取不到素材身份时:芯片显示画面侧可得的名字(对象名 → 几何类型 → 对象类型),不是一律的 `canvas`;矩形仍是命中对象在画面上的区域 | 真实 Chromium + 真实 three 0.184 + 真实桥脚本实测(瘦身句柄 `three: { Raycaster, Vector2, Vector3, Box3 }`):`object.name = 'Crate'` → 芯片 `Crate`、矩形 143×143;无名字的 `SphereGeometry` → 芯片 `SphereGeometry`、矩形 227×227;旧 `THREE` 命名空间句柄 → 芯片 `LegacyMesh`(两形态都命中);`userData.genarrativeResourceId` → `resourceIds: ["local-asset:probe"]`。两条 label 兜底与两形态句柄另由 `tests/runtimeInspectEngines.test.ts`(33 例)锁定。 |
素材收敛口径由 `tests/runtimeInspectResourceMatch.test.ts`(11 例)覆盖:显式 id、精确后缀、文件名、序列帧四档与去重。