File size: 11,905 Bytes
2cf467c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
"""
布局系统核心节点定义

基于 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"


# ============= 数据类定义 =============

@dataclass
class BoundingBox:
    """边界框 (x, y, width, height)"""
    x: float
    y: float
    width: float
    height: float
    
    @property
    def left(self) -> float:
        return self.x
    
    @property
    def right(self) -> float:
        return self.x + self.width
    
    @property
    def top(self) -> float:
        return self.y
    
    @property
    def bottom(self) -> float:
        return self.y + self.height
    
    @property
    def center_x(self) -> float:
        return self.x + self.width / 2
    
    @property
    def center_y(self) -> float:
        return self.y + self.height / 2


@dataclass
class Padding:
    """内边距"""
    top: float = 0
    right: float = 0
    bottom: float = 0
    left: float = 0
    
    @classmethod
    def uniform(cls, value: float) -> 'Padding':
        """创建统一的内边距"""
        return cls(value, value, value, value)
    
    @property
    def horizontal(self) -> float:
        """水平方向总内边距"""
        return self.left + self.right
    
    @property
    def vertical(self) -> float:
        """垂直方向总内边距"""
        return self.top + self.bottom


@dataclass
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  # 父节点引用
        
    @abstractmethod
    def compute_intrinsic_size(self) -> Tuple[float, float]:
        """
        计算节点的固有尺寸 (width, height)
        
        对于 Leaf Node: 返回内容的实际尺寸
        对于 Non-Leaf Node: 根据子节点和布局规则计算
        """
        pass
    
    @abstractmethod
    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
    
    @abstractmethod
    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
            }
        }