3Stark123 commited on
Commit
99a8a1e
·
verified ·
1 Parent(s): a7b9475

Create src/layout_engine.py

Browse files
Files changed (1) hide show
  1. src/layout_engine.py +500 -0
src/layout_engine.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ Layout engine for positioning and arranging infographic elements
4
+ """
5
+ from typing import Dict, List, Tuple, Optional
6
+ import math
7
+ import logging
8
+ from dataclasses import dataclass
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ @dataclass
13
+ class LayoutElement:
14
+ """Represents a positioned element in the layout"""
15
+ id: str
16
+ type: str
17
+ content: str
18
+ x: int
19
+ y: int
20
+ width: int
21
+ height: int
22
+ priority: int
23
+ styling: Dict
24
+
25
+ @dataclass
26
+ class LayoutGrid:
27
+ """Grid system for organizing layout"""
28
+ columns: int
29
+ rows: int
30
+ cell_width: int
31
+ cell_height: int
32
+ gap: int
33
+
34
+ class LayoutEngine:
35
+ """Engine for creating and managing infographic layouts"""
36
+
37
+ def __init__(self):
38
+ """Initialize layout engine"""
39
+ self.current_layout = None
40
+ self.elements = []
41
+ logger.info("Layout engine initialized")
42
+
43
+ def create_layout(self, styled_content: Dict) -> Dict:
44
+ """
45
+ Create complete layout from styled content
46
+
47
+ Args:
48
+ styled_content: Content with applied template styling
49
+
50
+ Returns:
51
+ Complete layout specification
52
+ """
53
+ layout_type = styled_content.get('layout_type', 'Vertical')
54
+ design_specs = styled_content.get('design_specs', {})
55
+
56
+ # Initialize layout canvas
57
+ canvas_size = design_specs.get('canvas_size', (1080, 1920))
58
+ margins = design_specs.get('margins', {'top': 60, 'bottom': 60, 'left': 60, 'right': 60})
59
+
60
+ layout = {
61
+ 'type': layout_type,
62
+ 'canvas_width': canvas_size[0],
63
+ 'canvas_height': canvas_size[1],
64
+ 'content_area': {
65
+ 'x': margins['left'],
66
+ 'y': margins['top'],
67
+ 'width': canvas_size[0] - margins['left'] - margins['right'],
68
+ 'height': canvas_size[1] - margins['top'] - margins['bottom']
69
+ },
70
+ 'elements': [],
71
+ 'grid': self._create_grid_system(layout_type, canvas_size, margins, design_specs),
72
+ 'flow': self._create_flow_system(layout_type)
73
+ }
74
+
75
+ # Position elements based on layout type
76
+ if layout_type == 'Vertical':
77
+ layout['elements'] = self._create_vertical_layout(styled_content, layout)
78
+ elif layout_type == 'Horizontal':
79
+ layout['elements'] = self._create_horizontal_layout(styled_content, layout)
80
+ elif layout_type == 'Grid':
81
+ layout['elements'] = self._create_grid_layout(styled_content, layout)
82
+ elif layout_type == 'Flow':
83
+ layout['elements'] = self._create_flow_layout(styled_content, layout)
84
+ else:
85
+ layout['elements'] = self._create_vertical_layout(styled_content, layout)
86
+
87
+ self.current_layout = layout
88
+ return layout
89
+
90
+ def _create_grid_system(self, layout_type: str, canvas_size: Tuple[int, int],
91
+ margins: Dict, design_specs: Dict) -> LayoutGrid:
92
+ """Create grid system for layout"""
93
+ grid_specs = design_specs.get('grid', {'columns': 1, 'rows': 'auto', 'gap': 30})
94
+
95
+ content_width = canvas_size[0] - margins['left'] - margins['right']
96
+ content_height = canvas_size[1] - margins['top'] - margins['bottom']
97
+
98
+ columns = grid_specs['columns']
99
+ gap = grid_specs['gap']
100
+
101
+ cell_width = (content_width - (columns - 1) * gap) // columns
102
+ cell_height = 200 # Default cell height, can be adjusted
103
+
104
+ return LayoutGrid(
105
+ columns=columns,
106
+ rows=grid_specs.get('rows', 'auto'),
107
+ cell_width=cell_width,
108
+ cell_height=cell_height,
109
+ gap=gap
110
+ )
111
+
112
+ def _create_flow_system(self, layout_type: str) -> Dict:
113
+ """Create flow system for dynamic positioning"""
114
+ return {
115
+ 'direction': 'vertical' if layout_type in ['Vertical', 'Flow'] else 'horizontal',
116
+ 'wrap': layout_type == 'Flow',
117
+ 'spacing': 'auto',
118
+ 'alignment': 'start'
119
+ }
120
+
121
+ def _create_vertical_layout(self, styled_content: Dict, layout: Dict) -> List[LayoutElement]:
122
+ """Create vertical layout arrangement"""
123
+ elements = []
124
+ current_y = layout['content_area']['y']
125
+ content_width = layout['content_area']['width']
126
+ content_x = layout['content_area']['x']
127
+
128
+ # Add title
129
+ title = styled_content.get('title', {})
130
+ if title.get('text'):
131
+ title_element = LayoutElement(
132
+ id='title',
133
+ type='title',
134
+ content=title['text'],
135
+ x=content_x,
136
+ y=current_y,
137
+ width=content_width,
138
+ height=self._calculate_text_height(title['text'], title.get('font', ('Arial', 32, 'bold')), content_width),
139
+ priority=10,
140
+ styling=title
141
+ )
142
+ elements.append(title_element)
143
+ current_y += title_element.height + title.get('margin', 40)
144
+
145
+ # Add sections
146
+ sections = styled_content.get('sections', [])
147
+ for i, section in enumerate(sections):
148
+ section_height = self._calculate_section_height(section, content_width)
149
+
150
+ section_element = LayoutElement(
151
+ id=f'section_{section.get("id", i)}',
152
+ type='section',
153
+ content=section.get('condensed_text', section.get('content', '')),
154
+ x=content_x,
155
+ y=current_y,
156
+ width=content_width,
157
+ height=section_height,
158
+ priority=section.get('priority', 5),
159
+ styling=section.get('styling', {})
160
+ )
161
+ elements.append(section_element)
162
+ current_y += section_height + section.get('styling', {}).get('margin', 30)
163
+
164
+ # Add visual elements
165
+ visual_elements = styled_content.get('visual_elements', [])
166
+ for element in visual_elements:
167
+ if element.get('placement') == 'body':
168
+ visual_element = self._create_visual_element(element, content_x, current_y, content_width)
169
+ elements.append(visual_element)
170
+ current_y += visual_element.height + 20
171
+
172
+ return elements
173
+
174
+ def _create_horizontal_layout(self, styled_content: Dict, layout: Dict) -> List[LayoutElement]:
175
+ """Create horizontal layout arrangement"""
176
+ elements = []
177
+ content_area = layout['content_area']
178
+
179
+ # Title spans full width
180
+ title = styled_content.get('title', {})
181
+ current_y = content_area['y']
182
+
183
+ if title.get('text'):
184
+ title_element = LayoutElement(
185
+ id='title',
186
+ type='title',
187
+ content=title['text'],
188
+ x=content_area['x'],
189
+ y=current_y,
190
+ width=content_area['width'],
191
+ height=self._calculate_text_height(title['text'], title.get('font', ('Arial', 32, 'bold')), content_area['width']),
192
+ priority=10,
193
+ styling=title
194
+ )
195
+ elements.append(title_element)
196
+ current_y += title_element.height + title.get('margin', 40)
197
+
198
+ # Split remaining sections into two columns
199
+ sections = styled_content.get('sections', [])
200
+ column_width = (content_area['width'] - 40) // 2 # 40px gap between columns
201
+
202
+ left_column_y = current_y
203
+ right_column_y = current_y
204
+
205
+ for i, section in enumerate(sections):
206
+ section_height = self._calculate_section_height(section, column_width)
207
+
208
+ if i % 2 == 0: # Left column
209
+ x = content_area['x']
210
+ y = left_column_y
211
+ left_column_y += section_height + 30
212
+ else: # Right column
213
+ x = content_area['x'] + column_width + 40
214
+ y = right_column_y
215
+ right_column_y += section_height + 30
216
+
217
+ section_element = LayoutElement(
218
+ id=f'section_{section.get("id", i)}',
219
+ type='section',
220
+ content=section.get('condensed_text', section.get('content', '')),
221
+ x=x,
222
+ y=y,
223
+ width=column_width,
224
+ height=section_height,
225
+ priority=section.get('priority', 5),
226
+ styling=section.get('styling', {})
227
+ )
228
+ elements.append(section_element)
229
+
230
+ return elements
231
+
232
+ def _create_grid_layout(self, styled_content: Dict, layout: Dict) -> List[LayoutElement]:
233
+ """Create grid layout arrangement"""
234
+ elements = []
235
+ grid = layout['grid']
236
+ content_area = layout['content_area']
237
+
238
+ # Title at top
239
+ title = styled_content.get('title', {})
240
+ current_y = content_area['y']
241
+
242
+ if title.get('text'):
243
+ title_element = LayoutElement(
244
+ id='title',
245
+ type='title',
246
+ content=title['text'],
247
+ x=content_area['x'],
248
+ y=current_y,
249
+ width=content_area['width'],
250
+ height=80,
251
+ priority=10,
252
+ styling=title
253
+ )
254
+ elements.append(title_element)
255
+ current_y += 120
256
+
257
+ # Arrange sections in grid
258
+ sections = styled_content.get('sections', [])
259
+ grid_start_y = current_y
260
+
261
+ for i, section in enumerate(sections):
262
+ row = i // grid.columns
263
+ col = i % grid.columns
264
+
265
+ x = content_area['x'] + col * (grid.cell_width + grid.gap)
266
+ y = grid_start_y + row * (grid.cell_height + grid.gap)
267
+
268
+ section_element = LayoutElement(
269
+ id=f'section_{section.get("id", i)}',
270
+ type='section',
271
+ content=section.get('condensed_text', section.get('content', '')),
272
+ x=x,
273
+ y=y,
274
+ width=grid.cell_width,
275
+ height=grid.cell_height,
276
+ priority=section.get('priority', 5),
277
+ styling=section.get('styling', {})
278
+ )
279
+ elements.append(section_element)
280
+
281
+ return elements
282
+
283
+ def _create_flow_layout(self, styled_content: Dict, layout: Dict) -> List[LayoutElement]:
284
+ """Create flowing layout arrangement"""
285
+ elements = []
286
+ content_area = layout['content_area']
287
+
288
+ # Title
289
+ title = styled_content.get('title', {})
290
+ current_y = content_area['y']
291
+
292
+ if title.get('text'):
293
+ title_element = LayoutElement(
294
+ id='title',
295
+ type='title',
296
+ content=title['text'],
297
+ x=content_area['x'],
298
+ y=current_y,
299
+ width=content_area['width'],
300
+ height=80,
301
+ priority=10,
302
+ styling=title
303
+ )
304
+ elements.append(title_element)
305
+ current_y += 100
306
+
307
+ # Flow sections dynamically
308
+ sections = styled_content.get('sections', [])
309
+ current_x = content_area['x']
310
+ row_height = 0
311
+ max_width = content_area['width']
312
+
313
+ for i, section in enumerate(sections):
314
+ section_width = min(400, max_width // 2) # Adaptive width
315
+ section_height = self._calculate_section_height(section, section_width)
316
+
317
+ # Check if we need to wrap to next row
318
+ if current_x + section_width > content_area['x'] + max_width:
319
+ current_x = content_area['x']
320
+ current_y += row_height + 30
321
+ row_height = 0
322
+
323
+ section_element = LayoutElement(
324
+ id=f'section_{section.get("id", i)}',
325
+ type='section',
326
+ content=section.get('condensed_text', section.get('content', '')),
327
+ x=current_x,
328
+ y=current_y,
329
+ width=section_width,
330
+ height=section_height,
331
+ priority=section.get('priority', 5),
332
+ styling=section.get('styling', {})
333
+ )
334
+ elements.append(section_element)
335
+
336
+ current_x += section_width + 20
337
+ row_height = max(row_height, section_height)
338
+
339
+ return elements
340
+
341
+ def _calculate_text_height(self, text: str, font: Tuple[str, int, str], width: int) -> int:
342
+ """Calculate approximate text height"""
343
+ if not text:
344
+ return 0
345
+
346
+ font_size = font[1] if len(font) > 1 else 16
347
+ chars_per_line = width // (font_size * 0.6) # Rough approximation
348
+ lines = max(1, len(text) / chars_per_line)
349
+ line_height = font_size * 1.4 # Standard line height
350
+
351
+ return int(lines * line_height)
352
+
353
+ def _calculate_section_height(self, section: Dict, width: int) -> int:
354
+ """Calculate section height based on content"""
355
+ content = section.get('condensed_text', section.get('content', ''))
356
+ styling = section.get('styling', {})
357
+ font = styling.get('font', ('Arial', 16, 'normal'))
358
+
359
+ base_height = self._calculate_text_height(content, font, width)
360
+ padding = styling.get('padding', 20)
361
+
362
+ return base_height + padding * 2
363
+
364
+ def _create_visual_element(self, element: Dict, x: int, y: int, max_width: int) -> LayoutElement:
365
+ """Create visual element layout"""
366
+ element_type = element.get('type', 'icon')
367
+ importance = element.get('importance', 5)
368
+
369
+ # Size based on importance
370
+ if element_type == 'chart':
371
+ width = min(max_width, 300)
372
+ height = 200
373
+ elif element_type == 'icon':
374
+ size = 32 + (importance * 4)
375
+ width = height = size
376
+ else:
377
+ width = min(max_width, 250)
378
+ height = 150
379
+
380
+ return LayoutElement(
381
+ id=f'visual_{element.get("type", "element")}_{id(element)}',
382
+ type=element_type,
383
+ content=element.get('description', ''),
384
+ x=x,
385
+ y=y,
386
+ width=width,
387
+ height=height,
388
+ priority=importance,
389
+ styling=element.get('styling', {})
390
+ )
391
+
392
+ def optimize_layout(self, layout: Dict) -> Dict:
393
+ """Optimize layout for better visual balance"""
394
+ elements = layout.get('elements', [])
395
+
396
+ # Remove overlapping elements
397
+ elements = self._resolve_overlaps(elements)
398
+
399
+ # Balance visual weight
400
+ elements = self._balance_visual_weight(elements, layout)
401
+
402
+ # Ensure minimum spacing
403
+ elements = self._enforce_minimum_spacing(elements)
404
+
405
+ layout['elements'] = elements
406
+ return layout
407
+
408
+ def _resolve_overlaps(self, elements: List[LayoutElement]) -> List[LayoutElement]:
409
+ """Resolve overlapping elements"""
410
+ for i, elem1 in enumerate(elements):
411
+ for j, elem2 in enumerate(elements[i+1:], i+1):
412
+ if self._elements_overlap(elem1, elem2):
413
+ # Move the lower priority element
414
+ if elem1.priority < elem2.priority:
415
+ elem1.y = elem2.y + elem2.height + 20
416
+ else:
417
+ elem2.y = elem1.y + elem1.height + 20
418
+
419
+ return elements
420
+
421
+ def _elements_overlap(self, elem1: LayoutElement, elem2: LayoutElement) -> bool:
422
+ """Check if two elements overlap"""
423
+ return not (elem1.x + elem1.width <= elem2.x or
424
+ elem2.x + elem2.width <= elem1.x or
425
+ elem1.y + elem1.height <= elem2.y or
426
+ elem2.y + elem2.height <= elem1.y)
427
+
428
+ def _balance_visual_weight(self, elements: List[LayoutElement], layout: Dict) -> List[LayoutElement]:
429
+ """Balance visual weight of elements"""
430
+ # Sort by priority
431
+ elements.sort(key=lambda x: x.priority, reverse=True)
432
+
433
+ # Adjust positions for better balance
434
+ canvas_center_x = layout['canvas_width'] // 2
435
+
436
+ for element in elements:
437
+ if element.type == 'title':
438
+ # Center titles
439
+ element.x = canvas_center_x - element.width // 2
440
+
441
+ return elements
442
+
443
+ def _enforce_minimum_spacing(self, elements: List[LayoutElement]) -> List[LayoutElement]:
444
+ """Ensure minimum spacing between elements"""
445
+ min_spacing = 15
446
+
447
+ # Sort by y position
448
+ elements.sort(key=lambda x: x.y)
449
+
450
+ for i in range(len(elements) - 1):
451
+ current = elements[i]
452
+ next_elem = elements[i + 1]
453
+
454
+ required_y = current.y + current.height + min_spacing
455
+ if next_elem.y < required_y:
456
+ next_elem.y = required_y
457
+
458
+ return elements
459
+
460
+ def get_layout_bounds(self, layout: Dict) -> Dict:
461
+ """Get the bounds of the entire layout"""
462
+ elements = layout.get('elements', [])
463
+
464
+ if not elements:
465
+ return {'x': 0, 'y': 0, 'width': 0, 'height': 0}
466
+
467
+ min_x = min(elem.x for elem in elements)
468
+ min_y = min(elem.y for elem in elements)
469
+ max_x = max(elem.x + elem.width for elem in elements)
470
+ max_y = max(elem.y + elem.height for elem in elements)
471
+
472
+ return {
473
+ 'x': min_x,
474
+ 'y': min_y,
475
+ 'width': max_x - min_x,
476
+ 'height': max_y - min_y
477
+ }
478
+
479
+ def export_layout_data(self, layout: Dict) -> Dict:
480
+ """Export layout data for image generation"""
481
+ return {
482
+ 'canvas': {
483
+ 'width': layout['canvas_width'],
484
+ 'height': layout['canvas_height'],
485
+ 'background': '#ffffff'
486
+ },
487
+ 'elements': [
488
+ {
489
+ 'id': elem.id,
490
+ 'type': elem.type,
491
+ 'content': elem.content,
492
+ 'position': {'x': elem.x, 'y': elem.y},
493
+ 'size': {'width': elem.width, 'height': elem.height},
494
+ 'priority': elem.priority,
495
+ 'styling': elem.styling
496
+ }
497
+ for elem in layout.get('elements', [])
498
+ ],
499
+ 'bounds': self.get_layout_bounds(layout)
500
+ }