# 多角度编辑器技术实现记录 ## 背景 本功能在 `TEST-s` 分支中为图片输入链路增加“图片多角度”生成能力。当前入口已从底部固定生成控制台迁移到画布连接菜单:用户创建图片输入节点并上传图片后,从图片输出端拖拽打开菜单,在“图片对比”下方点击“图片多角度”,弹出多角度编辑器,配置视角后生成新的图片生成节点。 核心文件: - `src/components/MultiAngleEditor.tsx` - `src/components/ConnectionDropMenu.tsx` - `src/components/WorkflowCanvas.tsx` - `src/components/composer/GenerationComposer.tsx` - `src/i18n/index.tsx` - `src/components/__tests__/MultiAngleEditor.test.tsx` - `src/components/__tests__/ConnectionDropMenu.test.tsx` - `src/components/__tests__/WorkflowCanvas.test.tsx` - `src/components/__tests__/GenerationComposer.test.tsx` ## 当前功能范围 已实现: - 从底部控制台移除“多角度”入口。 - 在图片输出拖拽菜单中新增“图片多角度”动作,位置位于“图片对比”下方。 - 点击“图片多角度”后打开独立浮层编辑器。 - 编辑器支持自定义、多种预设、地球仪视角仪表、摄像头视线、图片预览、提示词开关。 - 点击“生成多角度”后创建新的 `nanoBanana` 图片生成节点。 - 新节点会写入原图片作为 `inputImages`,并写入非空多角度提示词。 - 新节点会和源图片节点建立图片连线,并立即调用 `regenerateNode(newNodeId)`。 - 多角度提示词不包含外部平台名称。 未实现: - 真正 3D 场景渲染。当前地球仪是 SVG 视角仪表。 - 批量多角度生成。 - 将多角度参数持久化到源图片节点数据。 - 针对不同模型或服务商的提示词适配层。 ## 入口迁移 旧实现把“多角度”作为底部控制台的 chip: ```text 风格 / 标记 / 聚焦 / 多角度 ``` 本次调整后,底部控制台只保留常规生成和参考图能力,多角度不再在 `GenerationComposer` 中渲染。新的入口在 `ConnectionDropMenu` 的图片目标菜单中: ```ts export type MenuAction = "splitGridImmediate" | "multiAngleImage"; ``` `IMAGE_TARGET_OPTIONS` 中新增动作项,放在 `imageCompare` 后: ```ts { type: "multiAngleImage", label: "Image Multi-Angle", isAction: true, icon: ... } ``` 中文文案由 `src/i18n/index.tsx` 提供: ```text connectionMenu.multiAngleImage = 图片多角度 ``` ## 状态模型 多角度配置集中在 `src/components/MultiAngleEditor.tsx`: ```ts export interface MultiAngleSettings { preset: MultiAnglePresetId; horizontal: number; vertical: MultiAngleVertical; zoom: MultiAngleZoom; includePrompt: boolean; promptText: string; } ``` 字段含义: - `preset`:当前预设 ID。 - `horizontal`:水平环绕角度,范围 `0-315`。 - `vertical`:垂直俯仰,可选值 `-30 / 0 / 30 / 60`。 - `zoom`:景别缩放,`0=全景`,`1=中景`,`2=特写`。 - `includePrompt`:是否启用额外提示词输入框。 - `promptText`:用户输入或预设写入的额外提示词。 默认值: ```ts export const DEFAULT_MULTI_ANGLE_SETTINGS = { preset: "custom", horizontal: 0, vertical: -30, zoom: 1, includePrompt: false, promptText: "", }; ``` ## 预设配置 预设由 `MULTI_ANGLE_PRESET_SETTINGS` 统一维护。 | 预设 | horizontal | vertical | zoom | includePrompt | promptText | | --- | ---: | ---: | ---: | --- | --- | | 自定义 | 0 | -30 | 中景 | false | 空 | | 鱼眼视角 | 0 | 30 | 特写 | true | 超近距离鱼眼镜头,强烈超广角透视,圆形画面边界与黑色暗角,明显桶形畸变,画面中心主体放大,边缘向外弯曲拉伸 | | 倾斜视角 | 45 | -30 | 中景 | true | dutch angle, tilted frame | | 正面俯拍 | 0 | 60 | 中景 | false | 空 | | 正面仰拍 | 0 | -30 | 中景 | false | 空 | | 全景俯拍 | 45 | 30 | 全景 | false | 空 | | 背面视角 | 180 | 0 | 中景 | false | 空 | 点击预设会替换当前 `MultiAngleSettings`,并同步右侧参数、左侧地球仪、摄像头位置和提示词状态。 ## 视角仪表 `MultiAngleEditor` 内部用 SVG 实现地球仪式视角仪表。 主要元素: - 地球仪网格:`circle`、多组 `ellipse` 和中心基准线。 - 图片预览:始终在中心,仅受景别缩放影响。 - 摄像头:独立高层级 SVG,避免被图片遮挡。 - 视线:从摄像头连到图片中心。 水平环绕: ```ts const horizontalRadians = (settings.horizontal * Math.PI) / 180; const cameraX = 100 + Math.sin(horizontalRadians) * 78; ``` 垂直俯仰: ```ts const cameraY = 100 - (settings.vertical / 60) * 52; const globePitchRotation = -(settings.vertical / 60) * 16; const globePitchScaleY = 1 - Math.abs(settings.vertical) / 260; ``` 图片本体不随垂直俯仰上下移动,只有摄像头和地球仪网格随参数变化。 ## 鼠标交互 左侧地球仪区域支持拖拽: - 鼠标相对中心的角度换算为水平环绕。 - 鼠标垂直位置吸附到 `-30 / 0 / 30 / 60`。 - 拖拽更新 `MultiAngleSettings`。 - 右侧滑块调整时,左侧摄像头和地球仪同步更新。 地球仪区域使用: ```tsx data-composer-local-pointer="true" ``` 同时在事件回调中 `stopPropagation()`,避免拖拽被 React Flow 画布平移或选区逻辑截获。 ## 提示词策略 多角度提示词由 `buildMultiAnglePrompt(settings)` 生成。 模板: ```text 【多角度】基于输入图片生成,保持同一主体和画风。使用{水平描述},{垂直描述},景别为{景别}{可选额外提示词},构图稳定、画面自然。【/多角度】 ``` 水平提示词: ```text 0°: 水平环绕 0°,相机正对主体 非 0°: 水平环绕 {angle}°,按目标视角等效为主体向右转 {360 - angle}°,不要生成反向视角 ``` 垂直提示词: ```text vertical > 0: 垂直俯仰 {vertical}°,正面俯拍视角,相机从上方向下看主体 vertical < 0: 垂直俯仰 {vertical}°,正面仰拍视角,相机从下方向上看主体 ``` 即使 `includePrompt=false`,生成时也会发送基础多角度提示词,避免图片生成接口因空文本报 `Missing text input`。 ## 运行逻辑 `WorkflowCanvas` 维护浮层状态: ```ts interface MultiAngleDialogState { sourceNodeId: string; sourceHandleId: string; flowPosition: { x: number; y: number }; settings: MultiAngleSettings; } ``` 点击菜单动作时: 1. 读取当前连接拖拽状态。 2. 确认来源是图片输出。 3. 写入 `multiAngleDialog` 状态。 4. 关闭连接菜单。 点击“生成多角度”时: 1. 通过 `getImageFromNode(sourceNodeId)` 读取源图。 2. 创建新的 `nanoBanana` 节点。 3. 写入: - `inputImages: [sourceImage]` - `inputPrompt: buildMultiAnglePrompt(settings)` - 默认图像模型和基础生成参数 4. 创建从源图片节点到新生成节点的图片连线。 5. 选中新节点。 6. 关闭多角度浮层。 7. 调用 `regenerateNode(newNodeId)`。 相关实现函数: ```ts function buildMultiAngleNodeData( sourceImage: string, settings: MultiAngleSettings ): Partial ``` ## 测试覆盖 当前相关测试: ```text src/components/__tests__/MultiAngleEditor.test.tsx src/components/__tests__/ConnectionDropMenu.test.tsx src/components/__tests__/WorkflowCanvas.test.tsx src/components/__tests__/GenerationComposer.test.tsx ``` 覆盖点: - 多角度编辑器默认参数。 - 鱼眼预设参数和固定提示词。 - 地球仪拖拽和右侧参数同步。 - 多角度提示词不包含外部平台名称。 - 图片拖拽菜单包含“图片多角度”动作。 - “图片多角度”动作为 `isAction=true`,不会被当作普通节点创建。 - 画布菜单动作打开多角度浮层。 - 点击生成后创建并连接新的图片生成节点。 - 底部控制台不再渲染“多角度”入口。 建议验证命令: ```bash npm run test:run -- src/components/__tests__/MultiAngleEditor.test.tsx src/components/__tests__/ConnectionDropMenu.test.tsx src/components/__tests__/WorkflowCanvas.test.tsx src/components/__tests__/GenerationComposer.test.tsx ``` ## 维护注意点 - 新增多角度预设时,扩展 `MultiAnglePresetId`、`MULTI_ANGLE_PRESETS`、`MULTI_ANGLE_PRESET_SETTINGS` 和对应测试。 - 修改提示词语义时,优先调整 `buildMultiAnglePrompt()` 内部的水平/垂直描述函数。 - 不要把多角度入口重新放回 `GenerationComposer`,当前产品入口是图片输出拖拽菜单。 - 如果未来要做真实 3D 视角,应替换 `MultiAngleEditor` 内部 SVG 仪表,但保留外部 `MultiAngleSettings` 合约。 - `.env.local` 和服务商 API key 与本功能无关,本次实现没有改动任何服务商配置。