mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 20:23:55 +00:00
145 lines
4.1 KiB
Python
145 lines
4.1 KiB
Python
"""初始布局生成: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
|