feat:优化执行步骤

This commit is contained in:
2026-10-08 00:28:29 -07:00
parent 62a5b2a8f4
commit 05c050d2ba
20 changed files with 625 additions and 69 deletions
+20 -7
View File
@@ -5,10 +5,11 @@
from copy import deepcopy
from app.actions.layout import EXPANDERS
from app.actions.registry import apply_action
from app.actions.update import fit_existing_path
from app.resources.material_fields import check_update_field, find_material
from app.schemas.actions import Action, AddAction, UpdateAction
from app.schemas.actions import Action, AddAction, AgentAction, UpdateAction
from app.schemas.capabilities import DATA_BINDING_ERROR, DATA_BINDING_SUPPORTED
from app.schemas.component_resource import ComponentCatalog
from app.schemas.screen import Screen, ScreenNode
@@ -17,12 +18,15 @@ from app.schemas.screen import Screen, ScreenNode
class BatchError(ValueError):
"""整批中第 index 个动作(从 0 开始)失败,消息里带上序号和目标,模型据此修正"""
def __init__(self, index: int, action: Action, reason: str):
def __init__(self, index: int, action: AgentAction, reason: str):
self.index = index
self.reason = reason
target = getattr(action, "target_id", None)
if target is None:
target = action.node.id if isinstance(action, AddAction) else "画布"
if isinstance(action, AddAction):
target = action.node.id
elif ids := getattr(action, "target_ids", None):
target = "、".join(ids)
else:
target = getattr(action, "target_id", "画布")
super().__init__(f"第 {index + 1} 个动作({action.type} {target}):{reason}")
@@ -57,13 +61,22 @@ def complete_add(action: AddAction, catalog: ComponentCatalog) -> AddAction:
def run_batch(
screen: Screen, catalog: ComponentCatalog, actions: list[Action]
screen: Screen, catalog: ComponentCatalog, actions: list[AgentAction]
) -> tuple[list[Action], Screen]:
"""从原始大屏开始按顺序模拟执行,返回前端要执行的动作和执行后的大屏;第一个错误抛 BatchError"""
"""从原始大屏开始按顺序模拟执行,返回前端要执行的动作和执行后的大屏;第一个错误抛 BatchError
布局意图(arrange、align、distribute、set_layout)在这里按当时的大屏展开成 move 和 resize
"""
current = screen.model_copy(deep=True)
completed: list[Action] = []
for index, action in enumerate(actions):
try:
if expander := EXPANDERS.get(action.type):
expanded = expander(current, action)
for primitive in expanded:
current = apply_action(current, primitive)
completed.extend(expanded)
continue
if isinstance(action, AddAction):
action = complete_add(action, catalog)
elif isinstance(action, UpdateAction):
+152
View File
@@ -0,0 +1,152 @@
"""布局意图展开:算出每个组件的目标位置和尺寸,输出前端已有的 move 和 resize
纯函数,只读大屏;只输出有变化的动作。坐标取整,相邻组件共用同一条边,避免累积误差
"""
from math import ceil
from app.schemas.actions import (
AlignAction,
ArrangeAction,
DistributeAction,
MoveAction,
ResizeAction,
SetLayoutAction,
)
from app.schemas.actions.layout import Area
from app.schemas.screen import Screen, ScreenNode
Primitive = MoveAction | ResizeAction
def _nodes(screen: Screen, ids: list[str]) -> list[ScreenNode]:
by_id = {node.id: node for node in screen.nodes}
if missing := [item for item in ids if item not in by_id]:
raise ValueError(f"目标组件不存在: {'、'.join(missing)}")
return [by_id[item] for item in ids]
def _place(node: ScreenNode, x, y, width, height) -> list[Primitive]:
"""从当前布局变成目标布局,没变的项不输出"""
layout = node.layout
result: list[Primitive] = []
if (x, y) != (layout.x, layout.y):
result.append(
MoveAction(target_id=node.id, delta_x=x - layout.x, delta_y=y - layout.y)
)
if (width, height) != (layout.width, layout.height):
result.append(ResizeAction(target_id=node.id, width=width, height=height))
return result
def _canvas_area(screen: Screen) -> Area:
return Area(x=0, y=0, width=screen.canvas.width, height=screen.canvas.height)
def _bounds(nodes: list[ScreenNode]) -> Area:
left = min(n.layout.x for n in nodes)
top = min(n.layout.y for n in nodes)
right = max(n.layout.x + n.layout.width for n in nodes)
bottom = max(n.layout.y + n.layout.height for n in nodes)
return Area(x=left, y=top, width=right - left, height=bottom - top)
def arrange(screen: Screen, action: ArrangeAction) -> list[Primitive]:
nodes = _nodes(screen, action.target_ids)
count = len(nodes)
columns = {"row": count, "column": 1, "grid": action.columns}[action.mode]
rows = ceil(count / columns)
area = action.area or _canvas_area(screen)
left = area.x + action.padding
top = area.y + action.padding
cell_w = (area.width - 2 * action.padding - action.gap * (columns - 1)) / columns
cell_h = (area.height - 2 * action.padding - action.gap * (rows - 1)) / rows
if cell_w <= 0 or cell_h <= 0:
raise ValueError(
f"区域放不下 {count} 个组件:区域 {area.width:g}x{area.height:g},"
f"边距 {action.padding:g}、间距 {action.gap:g},减小间距或边距"
)
result: list[Primitive] = []
for index, node in enumerate(nodes):
row, column = divmod(index, columns)
x0 = left + column * (cell_w + action.gap)
y0 = top + row * (cell_h + action.gap)
# 取整的是边,不是宽度,相邻单元格不会因为取整多出缝隙或重叠
x, y = round(x0), round(y0)
result += _place(node, x, y, round(x0 + cell_w) - x, round(y0 + cell_h) - y)
return result
def align(screen: Screen, action: AlignAction) -> list[Primitive]:
nodes = _nodes(screen, action.target_ids)
box = _canvas_area(screen) if action.to == "canvas" else _bounds(nodes)
result: list[Primitive] = []
for node in nodes:
layout = node.layout
x, y = layout.x, layout.y
if action.horizontal == "left":
x = box.x
elif action.horizontal == "center":
x = round(box.x + (box.width - layout.width) / 2)
elif action.horizontal == "right":
x = box.x + box.width - layout.width
if action.vertical == "top":
y = box.y
elif action.vertical == "middle":
y = round(box.y + (box.height - layout.height) / 2)
elif action.vertical == "bottom":
y = box.y + box.height - layout.height
result += _place(node, x, y, layout.width, layout.height)
return result
def distribute(screen: Screen, action: DistributeAction) -> list[Primitive]:
horizontal = action.axis == "horizontal"
nodes = sorted(
_nodes(screen, action.target_ids),
key=lambda n: n.layout.x if horizontal else n.layout.y,
)
box = action.area or _bounds(nodes)
start, span = (box.x, box.width) if horizontal else (box.y, box.height)
sizes = [n.layout.width if horizontal else n.layout.height for n in nodes]
if action.gap is not None:
gap = action.gap
elif len(nodes) < 2:
raise ValueError("摊匀间距至少需要 2 个组件")
else:
gap = (span - sum(sizes)) / (len(nodes) - 1)
if gap < 0:
raise ValueError(
f"这些组件的{'宽' if horizontal else '高'}度合计 {sum(sizes):g},"
f"超过可用的 {span:g},排不开"
)
result: list[Primitive] = []
cursor = start
for node, size in zip(nodes, sizes, strict=True):
position = round(cursor)
layout = node.layout
x, y = (position, layout.y) if horizontal else (layout.x, position)
result += _place(node, x, y, layout.width, layout.height)
cursor += size + gap
return result
def set_layout(screen: Screen, action: SetLayoutAction) -> list[Primitive]:
(node,) = _nodes(screen, [action.target_id])
layout = node.layout
return _place(
node,
layout.x if action.x is None else action.x,
layout.y if action.y is None else action.y,
layout.width if action.width is None else action.width,
layout.height if action.height is None else action.height,
)
# 布局意图的 type -> 展开函数;新增意图时在这里注册
EXPANDERS = {
"arrange": arrange,
"align": align,
"distribute": distribute,
"set_layout": set_layout,
}