Skip to content

Commit be54c74

Browse files
committed
bug fix and refactoring
1 parent 1c72a39 commit be54c74

File tree

8 files changed

+212
-115
lines changed

8 files changed

+212
-115
lines changed

python/layout_visualizer.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from graphviz import Digraph
22
import json
3+
from typing import Optional
34
import argparse
45

5-
def tree_visualizer(json_path: str, output_path: str = "logs/layout_tree", format: str = "png"):
6-
print(output_path, format)
7-
with open(json_path, 'r', encoding='utf-8') as f:
8-
data = json.load(f)
6+
def visualize_layout_tree(data, output_path: str = "-", format: str = "png") -> Optional[str]:
7+
if isinstance(data, str):
8+
data = json.load(data)
99

1010
tree = data['final_tree']
1111

12-
dot = Digraph('LayoutTree', filename=output_path, format=format)
12+
dot = Digraph('LayoutTree', filename='layout_tree' if output_path == '-' else output_path, format=format)
1313
dot.attr("node", shape="box", fontname="Arial", fontsize="10")
1414

1515
def add_node(node):
@@ -19,29 +19,43 @@ def add_node(node):
1919
label += f'dir={node.get("direction", "-")}\n'
2020
label += f'size=({node["size"]["w"]}×{node["size"]["h"]})\n'
2121
label += f'pos=({node["position"]["x"]},{node["position"]["y"]})'
22+
color = node.get("color", "#dddddd")
2223
if node["type"] == "Text":
2324
txt = node.get("text_preview", "")
2425
preview = (txt[:20] + "…") if len(txt) > 20 else txt
2526
label += f'\n"{preview}"'
26-
color = node.get("backgroundColor", "#dddddd")
27+
color = "#dddddd"
2728
dot.node(nid, label, style="filled", fillcolor=color)
2829
for c in node.get("children", []):
2930
cid = str(c["node_id"])
3031
dot.edge(nid, cid)
3132
add_node(c)
3233
add_node(tree)
33-
dot.render(cleanup=True)
34-
print(f"[✓] Layout tree rendered to {output_path}.{format}")
34+
if output_path == '-':
35+
return dot.source
36+
else:
37+
dot.render(cleanup=True)
38+
print(f"[✓] Layout tree rendered to {output_path}.{format}")
39+
return None
40+
41+
3542

3643
if __name__ == "__main__":
3744
parser = argparse.ArgumentParser(description="Visualize layout tree from a layout JSON file.")
38-
parser.add_argument("--in", dest="input", required=True,
45+
parser.add_argument("input",
3946
help="Input JSON layout log (e.g., logs/layout-log.json)")
40-
parser.add_argument("--out", dest="output", default="layout_tree",
41-
help="Output file path without extension (default: layout_tree)")
47+
parser.add_argument("-o", "--out", default='-',
48+
help="Output file path or '-' for stdout (default: '-')")
4249
parser.add_argument("--format", default="png", choices=["png", "svg", "pdf"],
4350
help="Output format (default: png)")
4451

4552
args = parser.parse_args()
46-
47-
tree_visualizer(json_path=args.input)
53+
try:
54+
with open(args.input, 'r', encoding='utf-8') as f:
55+
data = json.load(f)
56+
except (FileNotFoundError, json.JSONDecodeError):
57+
data = args.input
58+
59+
result = visualize_layout_tree(data, args.out, args.format)
60+
if result:
61+
print(result)

python/rlc/layout.py

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from enum import Enum
2+
from typing import Optional, Tuple, List
23
import copy
34
# //utility for dumping layout
45

@@ -12,17 +13,17 @@ class SizePolicies(Enum):
1213
GROW = "grow"
1314

1415
class SizePolicy:
15-
def __init__(self, mode, value=None):
16-
self.mode = mode
17-
self.value = value
18-
def __repr__(self):
19-
return f"{self.mode}({self.value})" if self.value is not None else self.mode
16+
def __init__(self, size_policy: SizePolicies, value: Optional[float] = None):
17+
self.size_policy = size_policy
18+
self.value : Optional[int] = value
19+
def __repr__(self) -> str:
20+
return f"{self.size_policy.value}({self.value})" if self.value is not None else self.size_policy.value
2021

21-
def FIXED(value):
22+
def FIXED(value) -> SizePolicy:
2223
return SizePolicy(SizePolicies.FIXED, value)
23-
def FIT():
24+
def FIT() -> SizePolicy:
2425
return SizePolicy(SizePolicies.FIT, 0)
25-
def GROW():
26+
def GROW() -> SizePolicy:
2627
return SizePolicy(SizePolicies.GROW, 0)
2728

2829
class Padding:
@@ -34,27 +35,33 @@ def __init__(self, top=0, bottom=0, left=0, right=0):
3435

3536

3637
class Layout:
37-
def __init__(self, backgroundColor="white", sizing=(FIT(), FIT()), padding=Padding(0, 0, 0, 0), direction=Direction.ROW, child_gap=0):
38-
self.sizing = sizing
39-
self.backgroundColor = backgroundColor
40-
self.direction = direction
41-
self.child_gap = child_gap
42-
self.padding = padding
43-
self.children = []
38+
def __init__(
39+
self,
40+
sizing : Tuple[SizePolicy, SizePolicy] = (FIT(), FIT()),
41+
padding : Padding = Padding(0, 0, 0, 0),
42+
direction : Direction = Direction.ROW,
43+
child_gap : float = 0,
44+
color: Optional[str] = None):
45+
self.sizing : Tuple[SizePolicy, SizePolicy] = sizing
46+
self.direction : Direction = direction
47+
self.child_gap : float = child_gap
48+
self.padding : Padding = padding
49+
self.color: Optional[str] = color
50+
self.children : List['Layout'] = []
4451
self.x = 0
4552
self.y = 0
46-
self.width = sizing[0].value if sizing[0].mode == SizePolicies.FIXED else 0
47-
self.height = sizing[1].value if sizing[1].mode == SizePolicies.FIXED else 0
53+
self.width = sizing[0].value if sizing[0].size_policy == SizePolicies.FIXED else 0
54+
self.height = sizing[1].value if sizing[1].size_policy == SizePolicies.FIXED else 0
4855

49-
def add_child(self, child):
50-
self.children.append(child)
56+
def add_child(self, child: 'Layout') -> None:
57+
self.children.append(copy.deepcopy(child))
5158

52-
def _inner_dims(self):
59+
def _inner_dims(self) -> Tuple[float, float]:
5360
iw = max(0, self.width - self.padding.left - self.padding.right)
5461
ih = max(0, self.height - self.padding.top - self.padding.bottom)
5562
return iw, ih
5663

57-
def child_size(self,logger=None):
64+
def child_size(self,logger: Optional['LayoutLogger'] = None) -> None:
5865
# Always size children — even if self has fixed size
5966
inner_available_width = self.width - self.padding.left - self.padding.right
6067
inner_available_height = self.height - self.padding.top - self.padding.bottom
@@ -63,17 +70,17 @@ def child_size(self,logger=None):
6370
child_width_policy, child_height_policy = child.sizing
6471
# Only pass constraints if needed
6572
child_width = (
66-
inner_available_width if child_width_policy.mode != SizePolicies.FIXED else child_width_policy.value
73+
inner_available_width if child_width_policy.size_policy != SizePolicies.FIXED else child_width_policy.value
6774
)
6875
child_height = (
69-
inner_available_height if child_height_policy.mode != SizePolicies.FIXED else child_height_policy.value
76+
inner_available_height if child_height_policy.size_policy != SizePolicies.FIXED else child_height_policy.value
7077
)
7178
child.compute_size(child_width, child_height,logger)
7279

7380

7481
# Sizing
7582

76-
def compute_size(self, available_width=None, available_height=None, logger=None):
83+
def compute_size(self, available_width=None, available_height=None, logger: Optional['LayoutLogger']=None) -> None:
7784
if logger: logger.snapshot(self, "before_compute")
7885
if self.direction == Direction.ROW:
7986
self._compute_width()
@@ -91,25 +98,21 @@ def compute_size(self, available_width=None, available_height=None, logger=None)
9198
self._compute_grow_width()
9299
if logger: logger.snapshot(self, "after_compute")
93100

94-
def _compute_width(self):
101+
def _compute_width(self) -> None:
95102
width_policy, _ = self.sizing
96-
if width_policy.mode == SizePolicies.FIXED:
103+
if width_policy.size_policy == SizePolicies.FIXED:
97104
self.width = width_policy.value
98-
elif width_policy.mode == SizePolicies.FIT:
99-
self.width = self._compute_fit_width()
100-
# elif width_policy.mode == SizePolicies.GROW:
101-
# self.width = 0
105+
elif width_policy.size_policy == SizePolicies.FIT:
106+
self.width = self._compute_fit_width()
102107

103-
def _compute_height(self):
108+
def _compute_height(self) -> None:
104109
_, height_policy = self.sizing
105-
if height_policy.mode == SizePolicies.FIXED:
110+
if height_policy.size_policy == SizePolicies.FIXED:
106111
self.height = height_policy.value
107-
elif height_policy.mode == SizePolicies.FIT:
112+
elif height_policy.size_policy == SizePolicies.FIT:
108113
self.height = self._compute_fit_height()
109-
# elif height_policy.mode == SizePolicies.GROW:
110-
# self.height = 0
111114

112-
def _compute_fit_width(self):
115+
def _compute_fit_width(self) -> float:
113116
self.child_size()
114117
if self.direction == Direction.ROW:
115118
content_width = sum(c.width for c in self.children)
@@ -120,7 +123,7 @@ def _compute_fit_width(self):
120123

121124
return content_width + self.padding.left + self.padding.right
122125

123-
def _compute_fit_height(self):
126+
def _compute_fit_height(self) -> float:
124127
self.child_size()
125128
if self.direction == Direction.COLUMN:
126129
content_height = sum(c.height for c in self.children)
@@ -131,10 +134,10 @@ def _compute_fit_height(self):
131134

132135
return content_height + self.padding.top + self.padding.bottom
133136

134-
def _compute_grow_width(self):
137+
def _compute_grow_width(self) -> None:
135138
if self.direction == Direction.ROW:
136139
# Horizontal GROW
137-
total_fixed_width = sum(child.width for child in self.children if child.sizing[0].mode != SizePolicies.GROW)
140+
total_fixed_width = sum(child.width for child in self.children if child.sizing[0].size_policy != SizePolicies.GROW)
138141
total_gap = (len(self.children) - 1) * self.child_gap
139142
remaining_width = self.width - self.padding.left - self.padding.right - total_fixed_width - total_gap
140143
self._grow_children_evenly(self.children, remaining_width, axis=0)
@@ -143,26 +146,26 @@ def _compute_grow_width(self):
143146
# cross-axis GROW
144147
remaining_width = self.width - self.padding.left - self.padding.right
145148
for child in self.children:
146-
if child.sizing[0].mode == SizePolicies.GROW:
149+
if child.sizing[0].size_policy == SizePolicies.GROW:
147150
child.width = remaining_width
148151

149-
def _compute_grow_height(self):
152+
def _compute_grow_height(self) -> None:
150153
if self.direction == Direction.ROW:
151154
# cross-axis GROW
152155
remaining_height = self.height - self.padding.top - self.padding.bottom
153156
for child in self.children:
154-
if child.sizing[1].mode == SizePolicies.GROW:
157+
if child.sizing[1].size_policy == SizePolicies.GROW:
155158
child.height = remaining_height
156159

157160
if self.direction == Direction.COLUMN:
158161
# Vertical GROW
159-
total_fixed_height = sum(child.height for child in self.children if child.sizing[1].mode != SizePolicies.GROW)
162+
total_fixed_height = sum(child.height for child in self.children if child.sizing[1].size_policy != SizePolicies.GROW)
160163
total_gap = self.child_gap * (len(self.children) - 1)
161164
remaining_height = self.height - self.padding.top - self.padding.bottom - total_fixed_height - total_gap
162165
self._grow_children_evenly(self.children, remaining_height, axis=1)
163166

164-
def _grow_children_evenly(self, children, remaining, axis):
165-
growable = [c for c in children if c.sizing[axis].mode == SizePolicies.GROW]
167+
def _grow_children_evenly(self, children: List['Layout'], remaining: float, axis: int):
168+
growable = [c for c in children if c.sizing[axis].size_policy == SizePolicies.GROW]
166169
while growable and remaining > 0:
167170
growable.sort(key=lambda c: c.width if axis == 0 else c.height)
168171
smallest = growable[0]
@@ -192,7 +195,7 @@ def _grow_children_evenly(self, children, remaining, axis):
192195
c.height += grow_amount
193196
remaining -= grow_amount
194197
growable = [c for c in growable if (c.width if axis == 0 else c.height) < second_smallest]
195-
shrinkable = [c for c in children if c.sizing[axis].mode != SizePolicies.FIXED]
198+
shrinkable = [c for c in children if c.sizing[axis].size_policy != SizePolicies.FIXED]
196199
while shrinkable and remaining < 0:
197200
shrinkable.sort(key=lambda c: c.width if axis == 0 else c.height, reverse=True)
198201
largest = shrinkable[0]
@@ -227,7 +230,7 @@ def _grow_children_evenly(self, children, remaining, axis):
227230
shrinkable = [c for c in shrinkable if (c.width if axis == 0 else c.height) > second_largest_val]
228231

229232
# Position
230-
def layout(self, x=0, y=0, logger=None):
233+
def layout(self, x: int=0, y: int=0, logger: Optional['LayoutLogger']=None) -> None:
231234
self.x = x
232235
self.y = y
233236
if logger: logger.snapshot(self, "before_layout")
@@ -237,22 +240,38 @@ def layout(self, x=0, y=0, logger=None):
237240
self._layout_column_children()
238241
if logger: logger.snapshot(self, "after_layout")
239242

240-
def _layout_row_children(self):
243+
def _layout_row_children(self) -> None:
241244
left_offset = self.padding.left
242245
for child in self.children:
243246
child_pos_x = self.x + left_offset
244247
child_pos_y = self.y + self.padding.top
245248
child.layout(child_pos_x, child_pos_y)
246249
left_offset += child.width + self.child_gap
247250

248-
def _layout_column_children(self):
251+
def _layout_column_children(self) -> None:
249252
top_offset = self.padding.top
250253
for child in self.children:
251254
child_pos_x = self.x + self.padding.left
252255
child_pos_y = self.y + top_offset
253256
child.layout(child_pos_x, child_pos_y)
254257
top_offset += child.height + self.child_gap
255258

259+
def to_dot(self, dot: 'Diagraph', logger: Optional['LayoutLogger'] = None):
260+
"""Generate Graphviz DOT representation for this node and its children."""
261+
from graphviz import Digraph
262+
nid = str(logger._attach_id(self) if logger else id(self))
263+
label = f'{self.__class__.__name__}#{nid}\n'
264+
label += f'{self.sizing[0].size_policy.value}×{self.sizing[1].size_policy.value}\n'
265+
label += f'dir={self.direction.value if self.direction else "-"}\n'
266+
label += f'size=({self.width}×{self.height})\n'
267+
label += f'pos=({self.x},{self.y})'
268+
color = self.color if self.color else "#dddddd"
269+
dot.node(nid, label, style="filled", fillcolor=color)
270+
for child in self.children:
271+
child_nid = child.to_dot(dot, logger)
272+
dot.edge(nid, child_nid)
273+
return nid
274+
256275

257276

258277

python/rlc/layout_logger.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ def __del__(self):
3434
pass
3535

3636
def _attach_id(self, node) -> int:
37+
# Reset _log_node_id for copied nodes to ensure unique IDs
38+
if hasattr(node, "_log_node_id"):
39+
delattr(node, "_log_node_id")
3740
nid = getattr(node, "_log_node_id", None)
3841
if nid is None:
3942
nid = next(self._id_counter)
@@ -72,8 +75,8 @@ def _pad_str(self, pad) -> str:
7275

7376
def _extract_node_info(self, node) -> Dict[str, Any]:
7477
sizing = getattr(node, "sizing", (None, None))
75-
wmode = sizing[0].mode.value if sizing and sizing[0] else None
76-
hmode = sizing[1].mode.value if sizing and sizing[1] else None
78+
wmode = sizing[0].size_policy.value if sizing and sizing[0] else None
79+
hmode = sizing[1].size_policy.value if sizing and sizing[1] else None
7780
return {
7881
"id" : self._attach_id(node),
7982
"type": type(node).__name__,
@@ -203,13 +206,13 @@ def write_text_tree(self, root, path: str):
203206
def _tree_dict(self, node) -> Dict[str, Any]:
204207
nid = self._attach_id(node)
205208
sizing = getattr(node, "sizing", (None, None))
206-
wm = sizing[0].mode.value if sizing[0] else None
207-
hm = sizing[1].mode.value if sizing[1] else None
209+
wm = sizing[0].size_policy.value if sizing[0] else None
210+
hm = sizing[1].size_policy.value if sizing[1] else None
208211
info = self._extract_node_info(node)
209212
d : Dict[str, Any] = {
210213
"node_id" : info['id'],
211214
"type": info['type'],
212-
"backgroundColor": getattr(node, "backgroundColor", (220, 220, 220)),
215+
"color": getattr(node, "color", "#dddddd"),
213216
"direction": info['direction'],
214217
"position": info['position'],
215218
"size": info['size'],

0 commit comments

Comments
 (0)