Files
Uni-Lab-OS/unilabos/layout_optimizer/pencil_integration.py
2026-03-31 09:30:40 +08:00

145 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""初始布局生成Pencil MCP 接口 + 行列式回退。
策略:
1. 尝试调用 Pencil AI MCP 生成初始布局
2. 若 Pencil 不可用或失败,回退到行列式放置算法
行列式回退逻辑:
- 设备按面积从大到小排序
- 沿 X 轴逐个放置,行满(超出 lab.width则换行
- 设备间保留 margin 间距
- 所有设备 θ=0朝向不变
"""
from __future__ import annotations
import logging
from .models import Device, Lab, Placement
logger = logging.getLogger(__name__)
# 设备间最小间距(米)
DEFAULT_MARGIN = 0.3
def generate_initial_layout(
devices: list[Device],
lab: Lab,
margin: float = DEFAULT_MARGIN,
) -> list[Placement]:
"""生成初始布局方案。
优先尝试 Pencil MCP失败则回退到行列式放置。
Args:
devices: 待放置的设备列表
lab: 实验室平面图
margin: 设备间最小间距
Returns:
初始布局 Placement 列表
"""
# 尝试 Pencil MCP
pencil_result = _try_pencil(devices, lab)
if pencil_result is not None:
logger.info("Using Pencil AI generated layout")
return pencil_result
# 回退到行列式
logger.info("Pencil unavailable, using row-based fallback layout")
return generate_fallback(devices, lab, margin)
def _try_pencil(
devices: list[Device],
lab: Lab,
) -> list[Placement] | None:
"""尝试通过 Pencil AI MCP 生成布局。
当前 Pencil MCP 不可用,返回 None 触发回退。
未来集成时,此函数应:
1. 将设备 2D 投影 + 实验室平面图序列化为 Pencil 输入格式
2. 调用 mcp__pencil_* 工具
3. 解析返回的布局方案为 Placement 列表
预留接口参数:
- devices: 设备列表id, bbox
- lab: 实验室尺寸
"""
# TODO: 当 Pencil MCP 可用时实现
# 预期调用方式:
# pencil_input = {
# "floor_plan": {"width": lab.width, "depth": lab.depth},
# "items": [{"id": d.id, "width": d.bbox[0], "depth": d.bbox[1]} for d in devices],
# }
# result = mcp__pencil_layout(pencil_input)
# return [Placement(device_id=r["id"], x=r["x"], y=r["y"], theta=r["theta"]) for r in result]
return None
def generate_fallback(
devices: list[Device],
lab: Lab,
margin: float = DEFAULT_MARGIN,
) -> list[Placement]:
"""行列式回退布局:按面积从大到小排序,逐行放置。
放置规则:
- 设备中心坐标,从左上角开始
- 每行从 margin + half_width 开始
- 行满(下一个设备右边缘超出 lab.width - margin则换行
- 行高取该行最大设备深度
Args:
devices: 待放置的设备列表
lab: 实验室平面图
margin: 设备间最小间距
Returns:
Placement 列表。若实验室空间不足,剩余设备堆叠在右下角并记录警告。
"""
if not devices:
return []
# 按面积从大到小排序
sorted_devices = sorted(devices, key=lambda d: d.bbox[0] * d.bbox[1], reverse=True)
placements: list[Placement] = []
cursor_x = margin
cursor_y = margin
row_height = 0.0
for dev in sorted_devices:
w, d = dev.bbox
half_w = w / 2
half_d = d / 2
# 检查当前行是否放得下
if cursor_x + half_w + margin > lab.width and placements:
# 换行
cursor_x = margin
cursor_y += row_height + margin
row_height = 0.0
# 设备中心位置
cx = cursor_x + half_w
cy = cursor_y + half_d
# 检查是否超出实验室深度
if cy + half_d + margin > lab.depth:
logger.warning(
"Lab space insufficient for device '%s' (%s), "
"placing at overflow position",
dev.id,
dev.bbox,
)
placements.append(Placement(device_id=dev.id, x=cx, y=cy, theta=0.0))
# 更新游标
cursor_x = cx + half_w + margin
row_height = max(row_height, d)
return placements