Spaces:
Sleeping
Sleeping
| """ | |
| 布局系统核心节点定义 | |
| 基于 template.ebnf 实现的布局系统 | |
| 支持: | |
| - Flow Layout (ROW/COLUMN) | |
| - Non-Flow Layout (Z-layer) | |
| """ | |
| from abc import ABC, abstractmethod | |
| from typing import List, Optional, Tuple, Any | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| import numpy as np | |
| # ============= 枚举定义 ============= | |
| class NodeType(Enum): | |
| """节点类型""" | |
| GROUP = "GROUP" | |
| TEXT = "TEXT" | |
| IMAGE = "IMAGE" | |
| CHART = "CHART" | |
| SHAPE = "SHAPE" | |
| class LayoutType(Enum): | |
| """布局类型""" | |
| FLOW = "FLOW" | |
| NON_FLOW = "NON_FLOW" | |
| class FlowDirection(Enum): | |
| """Flow布局方向""" | |
| ROW = "ROW" # 水平排列 | |
| COLUMN = "COLUMN" # 垂直排列 | |
| # 未来扩展: | |
| # GRID = "GRID" | |
| # CIRCULAR = "CIRCULAR" | |
| # IRREGULAR = "IRREGULAR" | |
| class MainAlignment(Enum): | |
| """主轴对齐方式""" | |
| START = "START" | |
| CENTER = "CENTER" | |
| END = "END" | |
| class CrossAlignment(Enum): | |
| """交叉轴对齐方式""" | |
| START = "START" | |
| CENTER = "CENTER" | |
| END = "END" | |
| STRETCH = "STRETCH" | |
| class PositionAlign(Enum): | |
| """非流式布局位置对齐""" | |
| TOP_LEFT = "top-left" | |
| TOP_CENTER = "top-center" | |
| TOP_RIGHT = "top-right" | |
| CENTER_LEFT = "left" | |
| CENTER = "center" | |
| CENTER_RIGHT = "right" | |
| BOTTOM_LEFT = "bottom-left" | |
| BOTTOM_CENTER = "bottom-center" | |
| BOTTOM_RIGHT = "bottom-right" | |
| class Alignment(Enum): | |
| """对齐方式(用于每个节点自己的对齐)""" | |
| START = "START" | |
| CENTER = "CENTER" | |
| END = "END" | |
| LEFT = "LEFT" | |
| RIGHT = "RIGHT" | |
| # 特殊值(用于 layer 子节点) | |
| BACKGROUND = "BACKGROUND" | |
| TOP_LEFT = "TOP_LEFT" | |
| TOP_CENTER = "TOP_CENTER" | |
| TOP_RIGHT = "TOP_RIGHT" | |
| BOTTOM_LEFT = "BOTTOM_LEFT" | |
| BOTTOM_CENTER = "BOTTOM_CENTER" | |
| BOTTOM_RIGHT = "BOTTOM_RIGHT" | |
| # ============= 数据类定义 ============= | |
| class BoundingBox: | |
| """边界框 (x, y, width, height)""" | |
| x: float | |
| y: float | |
| width: float | |
| height: float | |
| def left(self) -> float: | |
| return self.x | |
| def right(self) -> float: | |
| return self.x + self.width | |
| def top(self) -> float: | |
| return self.y | |
| def bottom(self) -> float: | |
| return self.y + self.height | |
| def center_x(self) -> float: | |
| return self.x + self.width / 2 | |
| def center_y(self) -> float: | |
| return self.y + self.height / 2 | |
| class Padding: | |
| """内边距""" | |
| top: float = 0 | |
| right: float = 0 | |
| bottom: float = 0 | |
| left: float = 0 | |
| def uniform(cls, value: float) -> 'Padding': | |
| """创建统一的内边距""" | |
| return cls(value, value, value, value) | |
| def horizontal(self) -> float: | |
| """水平方向总内边距""" | |
| return self.left + self.right | |
| def vertical(self) -> float: | |
| """垂直方向总内边距""" | |
| return self.top + self.bottom | |
| class FlowAlignment: | |
| """Flow布局对齐配置""" | |
| main: MainAlignment = MainAlignment.START | |
| cross: CrossAlignment = CrossAlignment.START | |
| # ============= 抽象基类 ============= | |
| class Node(ABC): | |
| """节点抽象基类""" | |
| def __init__(self, node_id: str, node_type: NodeType, alignment: Optional[str] = None, parent: Optional['Node'] = None): | |
| self.id = node_id | |
| self.type = node_type | |
| self.alignment = alignment # 每个节点自己的对齐方式 | |
| self.bbox: Optional[BoundingBox] = None # 布局计算后的边界框 | |
| self.parent: Optional['Node'] = parent # 父节点引用 | |
| def compute_intrinsic_size(self) -> Tuple[float, float]: | |
| """ | |
| 计算节点的固有尺寸 (width, height) | |
| 对于 Leaf Node: 返回内容的实际尺寸 | |
| 对于 Non-Leaf Node: 根据子节点和布局规则计算 | |
| """ | |
| pass | |
| def layout(self, x: float, y: float, available_width: Optional[float] = None, | |
| available_height: Optional[float] = None) -> BoundingBox: | |
| """ | |
| 执行布局计算 | |
| Args: | |
| x: 起始 x 坐标 | |
| y: 起始 y 坐标 | |
| available_width: 可用宽度(可选) | |
| available_height: 可用高度(可选) | |
| Returns: | |
| 计算后的边界框 | |
| """ | |
| pass | |
| def to_dict(self) -> dict: | |
| """序列化为字典""" | |
| pass | |
| # ============= Leaf Node ============= | |
| class LeafNode(Node): | |
| """叶子节点 | |
| 叶子节点表示具体的视觉元素,具有固定的尺寸和可选的遮罩 | |
| """ | |
| def __init__(self, node_id: str, node_type: NodeType, | |
| width: float, height: float, | |
| mask: Optional[np.ndarray] = None, | |
| metadata: Optional[dict] = None, | |
| alignment: Optional[str] = None, | |
| parent: Optional[Node] = None): | |
| super().__init__(node_id, node_type, alignment, parent) | |
| self.width = width | |
| self.height = height | |
| self.mask = mask # 可选的二值遮罩,用于不规则形状 | |
| self.metadata = metadata or {} # 存储额外的元数据(如content, role, src等) | |
| def compute_intrinsic_size(self) -> Tuple[float, float]: | |
| """叶子节点的固有尺寸就是其宽高""" | |
| return (self.width, self.height) | |
| def layout(self, x: float, y: float, available_width: Optional[float] = None, | |
| available_height: Optional[float] = None) -> BoundingBox: | |
| """叶子节点的布局很简单,直接放置在指定位置""" | |
| self.bbox = BoundingBox(x, y, self.width, self.height) | |
| return self.bbox | |
| def to_dict(self) -> dict: | |
| result = { | |
| "id": self.id, | |
| "type": self.type.value, | |
| "bbox": { | |
| "x": self.bbox.x if self.bbox else 0, | |
| "y": self.bbox.y if self.bbox else 0, | |
| "width": self.width, | |
| "height": self.height | |
| } | |
| } | |
| # 添加元数据 | |
| if self.metadata: | |
| result.update(self.metadata) | |
| return result | |
| # ============= Non-Leaf Node ============= | |
| class GroupNode(Node): | |
| """组节点(非叶子节点) | |
| 组节点包含多个子节点,并根据布局类型对子节点进行排列 | |
| """ | |
| def __init__(self, node_id: str, layout_type: LayoutType, | |
| children: List[Node], padding: Optional[Padding] = None, | |
| alignment: Optional[str] = None, | |
| parent: Optional[Node] = None): | |
| super().__init__(node_id, NodeType.GROUP, alignment, parent) | |
| self.layout_type = layout_type | |
| self.children = children | |
| # 设置每个子节点的 parent | |
| for child in self.children: | |
| child.parent = self | |
| self.padding = padding or Padding() | |
| self.mask: Optional[np.ndarray] = None # 根据子节点mask合并得到 | |
| # 布局特定属性(子类设置) | |
| self.layout_attrs = {} | |
| def compute_intrinsic_size(self) -> Tuple[float, float]: | |
| """ | |
| 根据子节点和布局规则计算固有尺寸 | |
| 这个方法会在具体的布局子类中实现 | |
| """ | |
| raise NotImplementedError("Subclass must implement compute_intrinsic_size") | |
| def layout(self, x: float, y: float, available_width: Optional[float] = None, | |
| available_height: Optional[float] = None) -> BoundingBox: | |
| """ | |
| 执行组节点的布局 | |
| 具体的布局算法由子类实现 | |
| """ | |
| raise NotImplementedError("Subclass must implement layout") | |
| def _compute_mask_from_children(self): | |
| """ | |
| 根据所有子节点的mask计算父节点的mask | |
| 将每个子节点的mask根据其bbox位置转换到父节点坐标系, | |
| 然后合并所有mask(使用OR操作) | |
| """ | |
| if not self.bbox or not self.children: | |
| self.mask = None | |
| return | |
| # 收集所有有mask的子节点 | |
| children_with_mask = [ | |
| child for child in self.children | |
| if child.mask is not None and child.bbox is not None | |
| ] | |
| if not children_with_mask: | |
| self.mask = None | |
| return | |
| # 创建父节点的mask(全零) | |
| parent_width = int(self.bbox.width) | |
| parent_height = int(self.bbox.height) | |
| parent_mask = np.zeros((parent_height, parent_width), dtype=np.uint8) | |
| # 将每个子节点的mask转换到父节点坐标系并合并 | |
| for child in children_with_mask: | |
| child_mask = child.mask | |
| child_bbox = child.bbox | |
| # 计算子节点在父节点坐标系中的位置(相对于父节点左上角) | |
| child_x_in_parent = int(child_bbox.x - self.bbox.x) | |
| child_y_in_parent = int(child_bbox.y - self.bbox.y) | |
| child_width = int(child_bbox.width) | |
| child_height = int(child_bbox.height) | |
| # 确保子节点mask的尺寸与bbox一致 | |
| if child_mask.shape != (child_height, child_width): | |
| # 如果尺寸不匹配,调整mask尺寸(使用最近邻插值) | |
| mask_h, mask_w = child_mask.shape | |
| # 计算源坐标 | |
| y_coords = np.clip( | |
| (np.arange(child_height) * mask_h / child_height).astype(int), | |
| 0, mask_h - 1 | |
| ) | |
| x_coords = np.clip( | |
| (np.arange(child_width) * mask_w / child_width).astype(int), | |
| 0, mask_w - 1 | |
| ) | |
| # 使用numpy的高级索引进行最近邻插值 | |
| y_indices, x_indices = np.meshgrid(y_coords, x_coords, indexing='ij') | |
| child_mask = child_mask[y_indices, x_indices] | |
| # 计算在父节点mask中的位置范围 | |
| y_start = max(0, child_y_in_parent) | |
| y_end = min(parent_height, child_y_in_parent + child_height) | |
| x_start = max(0, child_x_in_parent) | |
| x_end = min(parent_width, child_x_in_parent + child_width) | |
| # 计算在子节点mask中的对应范围 | |
| child_y_start = max(0, -child_y_in_parent) | |
| child_y_end = child_y_start + (y_end - y_start) | |
| child_x_start = max(0, -child_x_in_parent) | |
| child_x_end = child_x_start + (x_end - x_start) | |
| # 将子节点mask复制到父节点mask的对应位置(OR操作) | |
| if (y_end > y_start and x_end > x_start and | |
| child_y_end > child_y_start and child_x_end > child_x_start): | |
| parent_mask[y_start:y_end, x_start:x_end] = np.maximum( | |
| parent_mask[y_start:y_end, x_start:x_end], | |
| child_mask[child_y_start:child_y_end, child_x_start:child_x_end] | |
| ) | |
| self.mask = parent_mask | |
| def to_dict(self) -> dict: | |
| return { | |
| "id": self.id, | |
| "type": self.type.value, | |
| "layout": self.layout_type.value, | |
| "layoutAttrs": self.layout_attrs, | |
| "children": [child.to_dict() for child in self.children], | |
| "bbox": { | |
| "x": self.bbox.x if self.bbox else 0, | |
| "y": self.bbox.y if self.bbox else 0, | |
| "width": self.bbox.width if self.bbox else 0, | |
| "height": self.bbox.height if self.bbox else 0 | |
| } | |
| } | |