feat:优化执行步骤
This commit is contained in:
+20
-7
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user