从照片到翻新方案:用 Pascal 3D 编辑器的 MCP Vision 工具链完成实景驱动装修规划
【免费下载链接】editorOpen-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.项目地址: https://gitcode.com/GitHub_Trending/editor93/editor
导读
本文讲解 Pascal 3D 编辑器(开源仓库GitHub_Trending/editor93/editor)MCP 服务器中一个完整的 Agent 实战工作流:如何将「renovation_from_photos提示词」与analyze_floorplan_image、analyze_room_photo两个视觉分析工具组合起来,以真实照片为事实依据,生成一套落地的装修翻新方案。读完本文,你将掌握从「用户丢进 4 张照片 + 一句需求」到「场景被墙、分区、开窗、家具全量建模并通过校验」的完整工具调用链路、底层 MCP Sampling 机制、Zod 输出校验原理,以及apply_patch原子化批量修改与undo/redo时间旅行的工程实现。
该示例文档位于仓库 packages/mcp/examples/renovate-from-photos.md,与同目录的 packages/mcp/examples/photo-to-scene.md(单张平面图直接建场景)互为姊妹篇。
背景:这条工作流解决什么问题
renovation_from_photos的核心目标是:让 AI Agent 基于"当前状态照片 + 参考风格照片"提出最小改动量的翻新方案,而不是凭空生成。它属于 Pascal MCP 服务器提供的三个提示词之一(完整清单见 packages/mcp/README.md):
| 提示词 | 参数 | 用途 |
|---|---|---|
from_brief | { brief, constraints? } | 从一段文字需求(如"80 m² 两居室")开始增量建场景 |
iterate_on_feedback | { feedback } | 以最小 diff 满足用户反馈 |
renovation_from_photos | { currentPhotos, referencePhotos, goals } | 串联视觉工具与场景变更工具,产出"照片驱动"的翻新计划 |
关键设计在于:视觉工具只返回数据、绝不修改场景,任何结构变更都必须由 Agent 通过apply_patch显式提出。这让整条链路可以审计、可回滚。
前置条件:宿主必须支持 MCP Sampling
两个视觉工具内部使用 MCP 的sampling(createMessage)能力把图片交给宿主模型推理:
- Claude Desktop 当前支持该能力,可直接运行此工作流;
- 不支持 sampling 的宿主会收到结构化的
sampling_unavailable错误; - 此时应回退到纯文本的
from_brief提示词,改由用户以文字描述现状。
在源码层面,该检查位于 packages/mcp/src/tools/vision/analyze-floorplan-image.ts 与 packages/mcp/src/tools/vision/analyze-room-photo.ts:工具先读取客户端能力server.server.getClientCapabilities(),若caps?.sampling为空则直接抛出McpError(ErrorCode.InvalidRequest, 'sampling_unavailable')。对应的测试用例见 packages/mcp/src/tools/vision/analyze-floorplan-image.test.ts。
用户需求(The Brief)
用户向聊天窗口投入四张照片:
- 平面图——PDF 页面导出为 PNG;
- 客厅现状照片;
- 厨房现状照片;
- 灵感参考图——一本杂志里的极简北欧 Loft 风格。
然后输入:
User:Claude, help me plan a renovation. Here's the current plan and two room photos. I want something like this Scandinavian reference — open-plan, neutral tones, keep the footprint.
这句话事实上已经携带了三个关键信息:保留 footprint(占地轮廓)、开放平面、中性色调——这些会成为goals参数的内容。
Agent 的执行流程
宿主加载renovation_from_photos提示词,其参数组装如下:
currentPhotos: ["data:image/png;base64,...", "data:image/jpeg;base64,..."] referencePhotos: ["data:image/jpeg;base64,..."] goals: "Open-plan living/kitchen, neutral tones, keep the footprint."提示词要求 Agent 按顺序执行五步:(1) 分析平面图,(2) 分析每张房间照片,(3) 依据平面图播种场景,(4) 与参考图对比,(5) 提出补丁方案。
提示词的源码实现与参数解析
提示词注册位于 packages/mcp/src/prompts/renovation-from-photos.ts,其argsSchema全部声明为字符串类型(MCP 提示词参数是 stringly-typed),并提供了buildRenovationMessages纯函数来构造完整的消息数组。内部有几层值得注意的健壮性处理:
- PREAMBLE 规则:要求对每张当前照片调用
analyze_floorplan_image和/或analyze_room_photo;对比现状与参考分析,找出与 goals 对齐的具体差异;最终只发出一次apply_patch,且包含收敛到目标所需的最小补丁集。同时硬性规定:不得编造尺寸(必须取自分析工具结果)、不得改动与目标无关的节点、只允许工具调用、不许输出散文。 - 图片输入归一化(toImageContent):
data:image/...;base64,...前缀会被剥离,mime 类型取自 URI,其余作为图片块(type: 'image');http(s)://URL 回退为文本URL: ...;- 裸 base64 通过保守检测(长度 ≥ 32、长度是 4 的倍数、仅含 base64 字符)后按
image/jpeg当作图片块; - 其余情况一律回退为文本,绝不猜测。
- 照片列表解析(parsePhotoList):支持 JSON 数组字符串与逗号分隔两种形式。
- 最终消息结构为:intro(含 Goals、图片数量统计)→ "## Current photos" → 各当前照片 → "## Reference photos" → 各参考照片 → "## Task"(要求只用分析工具得出的尺寸与家具生成
apply_patch)。
这些行为在 packages/mcp/src/prompts/prompts.test.ts 中有完整测试覆盖:JSON 数组解析、显式 mimeType 的 data URL、逗号分隔回退、空列表仅剩 intro + task 两条消息等。
1. 提取平面图(Extract the floorplan)
Agent 调用analyze_floorplan_image,把平面图 PNG 与比例提示一起交给视觉模型:
// tool: analyze_floorplan_image { "name": "analyze_floorplan_image", "arguments": { "image": "data:image/png;base64,iVBORw0KGgoAAAANS...", "scaleHint": "1 m grid, total footprint ~9.5 m × 7 m" } }底层原理(见 packages/mcp/src/tools/vision/analyze-floorplan-image.ts):
scaleHint是可选字符串,如"1 cm = 1 m"或"approximately 80 m²",会被拼进采样指令;- 工具通过
server.server.createMessage发起采样请求,temperature: 0、maxTokens: 2000,system prompt 强制模型只输出与 schema 严格一致的裸 JSON(禁止 markdown 围栏、禁止解释); - 返回文本先做 JSON 解析,再用
OutputSchema(Zod)做safeParse校验;解析失败抛sampling_response_unparseable,校验失败抛sampling_response_invalid(均带原始文本与错误明细); - 校验通过后,payload 同时写入
content(JSON 字符串)与structuredContent(结构化对象)两个通道。
工具的输入/输出 schema 如下(源码 analyzeFloorplanImageInput / analyzeFloorplanImageOutput):
- 输入:
image(base64 或 http(s) URL,必填)、scaleHint?; - 输出:
walls[](start/end为[x, z]二元组,thickness?)、rooms[](name、polygon、approximateAreaSqM?)、approximateDimensions(widthM、depthM)、confidence(0~1)。
一次典型返回:
{ "walls": [ { "start": [0, 0], "end": [9.5, 0], "thickness": 0.25 }, { "start": [9.5, 0], "end": [9.5, 7], "thickness": 0.25 }, { "start": [9.5, 7], "end": [0, 7], "thickness": 0.25 }, { "start": [0, 7], "end": [0, 0], "thickness": 0.25 }, { "start": [4.5, 0], "end": [4.5, 7], "thickness": 0.15 }, { "start": [4.5, 3.5], "end": [9.5, 3.5], "thickness": 0.15 } ], "rooms": [ { "label": "Living", "polygon": [[0, 0], [4.5, 0], [4.5, 7], [0, 7]] }, { "label": "Kitchen", "polygon": [[4.5, 0], [9.5, 0], [9.5, 3.5], [4.5, 3.5]] }, { "label": "Bedroom", "polygon": [[4.5, 3.5], [9.5, 3.5], [9.5, 7], [4.5, 7]] } ], "approximateDimensions": { "widthMeters": 9.5, "depthMeters": 7, "areaSqMeters": 66.5 }, "confidence": 0.82 }注意
label与widthMeters等字段在示例中为文档演示写法,源码 schema 中对应字段为name、widthM/depthM;坐标一律以米为单位,原点取平面图中心或左下角并保持一致(见 SYSTEM_PROMPT)。
图片来源安全:当image是http(s)://URL 时,工具不会直接fetch,而是走 packages/mcp/src/lib/safe-fetch.ts 的SSRF 防护抓取——阻止回环地址、链路本地(含云元数据 169.254.169.254)、私网段、非 http(s) 协议;限制最大 20 MB、10 秒超时、最多 3 次重定向且每次跳转都重新做允许名单校验;还支持PASCAL_ALLOWED_ASSET_ORIGINS环境变量追加来源白名单。抓取后按content-type嗅探 mime 类型再内联为 base64。
2. 分析房间照片(Analyze the room photos)
对客厅、厨房照片分别调用analyze_room_photo:
// tool: analyze_room_photo { "name": "analyze_room_photo", "arguments": { "image": "data:image/jpeg;base64,/9j/4AAQ..." } }返回示例:
{ "approximateDimensions": { "widthMeters": 4.4, "depthMeters": 5.8, "heightMeters": 2.5 }, "identifiedFixtures": [ { "kind": "sofa", "approximatePosition": [2.2, 3.5] }, { "kind": "coffee-table", "approximatePosition": [2.2, 2.4] }, { "kind": "tv-unit", "approximatePosition": [0.3, 2.0] } ], "identifiedWindows": [ { "wallHint": "south", "approximateWidth": 1.4, "approximateHeight": 1.5 } ] }源码中的真实 schema 见 analyzeRoomPhotoOutput:approximateDimensions(widthM、lengthM、heightM?)、identifiedFixtures[](type、approximatePosition?)、identifiedWindows[](wallLabel?、approximateWidthM?、approximateHeightM?)。system prompt 明确要求:估不准的度量宁可省略可选字段,也不要瞎猜;fixture 的type是短语,如"sofa"、"kitchen island"、"door"。厨房照片按同样的方式分析。
两个视觉工具都标记为READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS(readOnlyHint: true、idempotentHint: true、openWorldHint: true,见 packages/mcp/src/tools/annotations.ts),即"只读、幂等、面向开放世界",从注解层面保证它们不会改动场景。
3. 播种场景(Seed the scene)
Agent 先读取get_scene,确认默认的空Site → Building → Level骨架存在,然后按平面图结果批量创建墙体:
// tool: apply_patch { "name": "apply_patch", "arguments": { "patches": [ { "op": "create", "parentId": "level-1", "node": { "type": "wall", "start": [0, 0], "end": [9.5, 0], "thickness": 0.25, "height": 2.5 } }, /* ...remaining perimeter + partition walls from the vision result... */ ] } }接着调用三次set_zone,把平面图识别出的 Living / Kitchen / Bedroom 多边形播种为分区。
坐标约定提醒:Pascal 是右手坐标系,X、Z 构成地平面,Y 朝上,长度单位为米。wall.start/wall.end、zone.polygon等二维点都是[x, z]形式,第二个分量是世界的 Z(深度)而不是"上"(详见 packages/mcp/README.md 的 Coordinate conventions 一节)。因此平面图分析工具返回的[x, z]元组可以直接映射到墙体和分区,无需翻轴。
4. 开洞(Cut the identified openings)
对视觉工具报告出的每一扇窗,Agent 在对应外墙上调cut_opening:
{ "name": "cut_opening", "arguments": { "wallId": "wall-south", "type": "window", "position": 0.5, "width": 1.4, "height": 1.5 } }cut_opening的position是0..1 的沿墙归一化比例,内部会换算并以"墙局部米"存储(见 packages/mcp/README.md 的工具表)。视觉分析给出的"南墙 1.4 m 宽 × 1.5 m 高"窗户由此转成精确的开洞参数。
5. 提出翻新方案(Propose the renovation)
受参考图分析结果的引导(明亮中性色、开放平面、极简陈设),Agent 提出单一逻辑补丁:
- 拆除 Living 与 Kitchen 之间的隔墙;
- 把厨房中岛向西移动;
- 删除笨重的电视柜 item,保留沙发与茶几;
- 将合并后的分区重命名为
"Open-Plan Living / Kitchen"。
全部操作放进一次apply_patch:
{ "name": "apply_patch", "arguments": { "patches": [ { "op": "delete", "id": "wall-partition-living-kitchen", "cascade": false }, { "op": "update", "id": "zone-living", "data": { "label": "Open-Plan Living / Kitchen", "polygon": [[0, 0], [9.5, 0], [9.5, 3.5], [0, 3.5]] } }, { "op": "delete", "id": "zone-kitchen", "cascade": false } /* + item moves / deletes for the TV unit etc. */ ] } }apply_patch的原子性保证(见 packages/mcp/src/tools/apply-patch.ts):
- 输入为
patches[],每个 patch 支持create/update/delete三种 op,均通过PatchSchema校验; - 所有补丁先整体验证,再逐个应用——任何一个非法都会让整批失败,不会出现半改状态;
- 整批操作被 Zundo 时间中间件捕获为一个可撤销的时间步;
- 返回值包含
appliedOps、deletedIds、createdIds,并通过publishLiveSceneSnapshot触发实时同步(若编辑器与 MCP 共享PASCAL_DATA_DIR,浏览器标签页可通过/api/scenes/:id/events的 SSE 流实时看到 Agent 的每次改动)。
正因如此,用户随时可以用undo回退到翻新前,用redo回到翻新方案——整条链路天然支持"前后对比"。
6. 收尾校验(Sanity-check)
Agent 最后做两层一致性检查:
// tool: validate_scene { "name": "validate_scene", "arguments": {} } // → { "valid": true, "errors": [] } // tool: check_collisions { "name": "check_collisions", "arguments": { "levelId": "level-1" } } // → { "collisions": [] }validate_scene:用 Zod 逐个校验节点并检查父子完整性({ valid, errors: { nodeId, path, message }[] });check_collisions:查找重叠 item 与越界摆放({ collisions: { aId, bId, kind }[] })。
随后 Agent 汇报变更摘要 + 近似可用面积(来自pascal://scene/current/summary资源),用户即可在@pascal-app/viewer中打开场景查看翻新后的 3D 布局。
关键经验(Takeaways)
- 视觉工具只返回数据。
analyze_floorplan_image与analyze_room_photo被标记为只读/幂等(READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS),从不直接改动场景;每一次结构变更都由 Agent 通过apply_patch显式声明,整个流程可审计、可追溯。 - 照片提供了纯文字简报无法提供的信息——近似尺寸、家具类型、窗户位置等先验数据。当用户同时给出参考图与具体文字目标时,把本工作流与
from_brief风格的提示词结合使用,效果最佳。 - 每个翻新步骤都是单一时间步:一次
apply_patch= 一个可撤销单位,配合undo/redo,用户可以在任意时刻对比"改造前 / 改造后"。
延伸阅读
- 提示词实现与注册:packages/mcp/src/prompts/renovation-from-photos.ts
- 视觉工具实现与测试:packages/mcp/src/tools/vision/analyze-floorplan-image.ts、packages/mcp/src/tools/vision/analyze-room-photo.ts、packages/mcp/src/tools/vision/analyze-floorplan-image.test.ts
- 原子化补丁工具:packages/mcp/src/tools/apply-patch.ts
- SSRF 防护抓取:packages/mcp/src/lib/safe-fetch.ts
- 提示词测试:packages/mcp/src/prompts/prompts.test.ts
- 姊妹示例(单图建场景):packages/mcp/examples/photo-to-scene.md
- MCP 服务器总览与全部工具/资源/提示词清单:packages/mcp/README.md
【免费下载链接】editorOpen-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.项目地址: https://gitcode.com/GitHub_Trending/editor93/editor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考