diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-web-game-development/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-web-game-development/SKILL.md index 89b0088de..8f8c37647 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-web-game-development/SKILL.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-web-game-development/SKILL.md @@ -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', '')`,或 `sprite.setData('genarrativeResourcePath', '')`;两者都给时 id 优先。 diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index 028881a46..3b67e87e9 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -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", diff --git a/apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js b/apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js index ade83b6c9..5ff19cd48 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js +++ b/apps/ai-game-creator-shell/src-tauri/resources/preview/local-preview-fit.js @@ -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); diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js index 2febd40ff..abf55cfd2 100644 --- a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js @@ -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, diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js index dc0ed61ac..d83345b5e 100644 --- a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js @@ -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, diff --git a/apps/ai-game-creator-shell/tests/runtimeInspectEngines.test.ts b/apps/ai-game-creator-shell/tests/runtimeInspectEngines.test.ts index 087be4370..1457b2ec1 100644 --- a/apps/ai-game-creator-shell/tests/runtimeInspectEngines.test.ts +++ b/apps/ai-game-creator-shell/tests/runtimeInspectEngines.test.ts @@ -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; 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('运行画面点选:退化路径', () => { diff --git a/docs/project-memory/plans/【实施计划】AGC运行画面点选引擎适配-2026-10-04.md b/docs/project-memory/plans/【实施计划】AGC运行画面点选引擎适配-2026-10-04.md index 49262159d..84044dd88 100644 --- a/docs/project-memory/plans/【实施计划】AGC运行画面点选引擎适配-2026-10-04.md +++ b/docs/project-memory/plans/【实施计划】AGC运行画面点选引擎适配-2026-10-04.md @@ -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` 通过。 diff --git a/docs/project-memory/plans/【里程碑】AGC运行画面点选引擎适配-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC运行画面点选引擎适配-2026-10-04.md index fe565a475..d0d9d2e14 100644 --- a/docs/project-memory/plans/【里程碑】AGC运行画面点选引擎适配-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC运行画面点选引擎适配-2026-10-04.md @@ -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)、高亮真机目视。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 61f2bcd02..85bada0df 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -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 画面时,按「数组第一个 / 最后一个」当最上层会点错对象——点的是上层精灵,引用却落到下层的素材上;对象越多越容易错。 diff --git a/docs/【功能说明】AGC聊天素材引用-2026-09-08.md b/docs/【功能说明】AGC聊天素材引用-2026-09-08.md index e55799474..275e72275 100644 --- a/docs/【功能说明】AGC聊天素材引用-2026-09-08.md +++ b/docs/【功能说明】AGC聊天素材引用-2026-09-08.md @@ -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、精确后缀、文件名、序列帧四档与去重。