py2max 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. py2max/__init__.py +67 -0
  2. py2max/__main__.py +6 -0
  3. py2max/cli.py +1251 -0
  4. py2max/core/__init__.py +39 -0
  5. py2max/core/abstract.py +146 -0
  6. py2max/core/box.py +231 -0
  7. py2max/core/common.py +19 -0
  8. py2max/core/patcher.py +1658 -0
  9. py2max/core/patchline.py +68 -0
  10. py2max/exceptions.py +385 -0
  11. py2max/export/__init__.py +20 -0
  12. py2max/export/converters.py +345 -0
  13. py2max/export/svg.py +393 -0
  14. py2max/layout/__init__.py +26 -0
  15. py2max/layout/base.py +463 -0
  16. py2max/layout/flow.py +405 -0
  17. py2max/layout/grid.py +374 -0
  18. py2max/layout/matrix.py +628 -0
  19. py2max/log.py +338 -0
  20. py2max/maxref/__init__.py +78 -0
  21. py2max/maxref/category.py +163 -0
  22. py2max/maxref/db.py +1082 -0
  23. py2max/maxref/legacy.py +324 -0
  24. py2max/maxref/parser.py +703 -0
  25. py2max/py.typed +0 -0
  26. py2max/server/__init__.py +54 -0
  27. py2max/server/client.py +295 -0
  28. py2max/server/inline.py +312 -0
  29. py2max/server/repl.py +561 -0
  30. py2max/server/rpc.py +240 -0
  31. py2max/server/websocket.py +997 -0
  32. py2max/static/cola.min.js +4 -0
  33. py2max/static/d3.v7.min.js +2 -0
  34. py2max/static/dagre-bundle.js +328 -0
  35. py2max/static/elk.bundled.js +6663 -0
  36. py2max/static/index.html +168 -0
  37. py2max/static/interactive.html +589 -0
  38. py2max/static/interactive.js +2111 -0
  39. py2max/static/live-preview.js +324 -0
  40. py2max/static/svg.min.js +13 -0
  41. py2max/static/svg.min.js.map +1 -0
  42. py2max/transformers.py +168 -0
  43. py2max/utils.py +83 -0
  44. py2max-0.2.1.dist-info/METADATA +390 -0
  45. py2max-0.2.1.dist-info/RECORD +48 -0
  46. py2max-0.2.1.dist-info/WHEEL +4 -0
  47. py2max-0.2.1.dist-info/entry_points.txt +3 -0
  48. py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
py2max/layout/flow.py ADDED
@@ -0,0 +1,405 @@
1
+ """Signal flow-based layout manager for py2max patches.
2
+
3
+ This module provides FlowLayoutManager for intelligent signal flow-based layouts.
4
+ """
5
+
6
+ from typing import Dict, List, Optional, Set
7
+
8
+ from py2max.core.abstract import AbstractPatcher
9
+ from py2max.core.common import Rect
10
+
11
+ from .base import LayoutManager
12
+
13
+
14
+ class FlowLayoutManager(LayoutManager):
15
+ """Advanced layout manager that analyzes signal flow topology.
16
+
17
+ This layout manager:
18
+ - Analyzes patchline connections to understand signal flow
19
+ - Groups related objects based on connection patterns
20
+ - Uses hierarchical positioning with signal flow left-to-right or top-to-bottom
21
+ - Minimizes line crossings and connection distances
22
+ - Balances layout aesthetically while respecting functional relationships
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ parent: AbstractPatcher,
28
+ pad: Optional[int] = None,
29
+ box_width: Optional[int] = None,
30
+ box_height: Optional[int] = None,
31
+ comment_pad: Optional[int] = None,
32
+ flow_direction: str = "horizontal",
33
+ ):
34
+ super().__init__(parent, pad, box_width, box_height, comment_pad)
35
+ self._object_positions: Dict[str, Rect] = {} # Cache for calculated positions
36
+ self._flow_levels: Dict[str, int] = {} # Track hierarchical flow levels
37
+ self._position_cache: Dict[
38
+ str, Rect
39
+ ] = {} # Cache positions to avoid recalculation
40
+ self.flow_direction = flow_direction # "horizontal" or "vertical"
41
+
42
+ def _analyze_connections(self) -> dict:
43
+ """Analyze patchline connections to build a flow graph."""
44
+ connections: Dict[
45
+ str, Dict[str, List[str]]
46
+ ] = {} # {obj_id: {'inputs': [obj_ids], 'outputs': [obj_ids]}}
47
+
48
+ for line in self.parent._lines:
49
+ src_id, dst_id = line.src, line.dst
50
+
51
+ # Skip if either ID is None
52
+ if src_id is None or dst_id is None:
53
+ continue
54
+
55
+ # Initialize connection tracking
56
+ if src_id not in connections:
57
+ connections[src_id] = {"inputs": [], "outputs": []}
58
+ if dst_id not in connections:
59
+ connections[dst_id] = {"inputs": [], "outputs": []}
60
+
61
+ # Track connections
62
+ connections[src_id]["outputs"].append(dst_id)
63
+ connections[dst_id]["inputs"].append(src_id)
64
+
65
+ return connections
66
+
67
+ def _calculate_flow_levels(self, connections: dict) -> dict:
68
+ """Calculate hierarchical flow levels for objects based on signal chain depth."""
69
+ levels = {}
70
+ visited = set()
71
+
72
+ # Find source objects (no inputs)
73
+ sources = [obj_id for obj_id, conn in connections.items() if not conn["inputs"]]
74
+
75
+ if not sources:
76
+ # If no clear sources, find objects with minimal inputs
77
+ min_inputs = (
78
+ min(len(conn["inputs"]) for conn in connections.values())
79
+ if connections
80
+ else 0
81
+ )
82
+ sources = [
83
+ obj_id
84
+ for obj_id, conn in connections.items()
85
+ if len(conn["inputs"]) == min_inputs
86
+ ]
87
+
88
+ # Assign levels using BFS-like traversal
89
+ current_level = 0
90
+ current_objects = sources
91
+
92
+ while current_objects:
93
+ next_objects = []
94
+ for obj_id in current_objects:
95
+ if obj_id not in visited:
96
+ levels[obj_id] = current_level
97
+ visited.add(obj_id)
98
+
99
+ # Add outputs to next level
100
+ if obj_id in connections:
101
+ for output_id in connections[obj_id]["outputs"]:
102
+ if output_id not in visited:
103
+ next_objects.append(output_id)
104
+
105
+ current_objects = list(set(next_objects))
106
+ current_level += 1
107
+
108
+ # Handle disconnected objects
109
+ for obj_id in self.parent._objects:
110
+ if obj_id not in levels:
111
+ levels[obj_id] = current_level
112
+
113
+ return levels
114
+
115
+ def _group_by_level(self, levels: dict) -> dict:
116
+ """Group objects by their flow level."""
117
+ groups: Dict[int, List[str]] = {}
118
+ for obj_id, level in levels.items():
119
+ if level not in groups:
120
+ groups[level] = []
121
+ groups[level].append(obj_id)
122
+ return groups
123
+
124
+ def _minimize_crossings(
125
+ self, groups: Dict[int, List[str]], connections: dict
126
+ ) -> Dict[int, List[str]]:
127
+ """Minimize line crossings using the barycenter heuristic.
128
+
129
+ For each level (after the first), objects are reordered based on
130
+ the average position (barycenter) of their connected objects in
131
+ the previous level. This tends to place connected objects near
132
+ each other, reducing line crossings.
133
+
134
+ Args:
135
+ groups: Objects grouped by level
136
+ connections: Connection graph from _analyze_connections()
137
+
138
+ Returns:
139
+ Reordered groups with minimized crossings
140
+ """
141
+ if len(groups) < 2:
142
+ return groups
143
+
144
+ sorted_levels = sorted(groups.keys())
145
+ result: Dict[int, List[str]] = {}
146
+
147
+ # First level stays as-is (sorted for consistency)
148
+ first_level = sorted_levels[0]
149
+ result[first_level] = sorted(groups[first_level])
150
+
151
+ # Process subsequent levels
152
+ for i, level in enumerate(sorted_levels[1:], 1):
153
+ prev_level = sorted_levels[i - 1]
154
+ prev_objects = result[prev_level]
155
+ current_objects = groups[level]
156
+
157
+ # Create position map for previous level objects
158
+ prev_positions = {obj_id: idx for idx, obj_id in enumerate(prev_objects)}
159
+
160
+ # Calculate barycenter for each object in current level
161
+ barycenters: Dict[str, float] = {}
162
+ for obj_id in current_objects:
163
+ # Find all connections to previous level
164
+ connected_positions = []
165
+
166
+ # Check inputs (connections from previous level)
167
+ if obj_id in connections:
168
+ for input_id in connections[obj_id]["inputs"]:
169
+ if input_id in prev_positions:
170
+ connected_positions.append(prev_positions[input_id])
171
+
172
+ if connected_positions:
173
+ # Barycenter is the average position of connected objects
174
+ barycenters[obj_id] = sum(connected_positions) / len(
175
+ connected_positions
176
+ )
177
+ else:
178
+ # No connections to previous level - use a large value to push to end
179
+ barycenters[obj_id] = float("inf")
180
+
181
+ # Sort objects by their barycenter values
182
+ sorted_objects = sorted(
183
+ current_objects,
184
+ key=lambda obj_id: (barycenters[obj_id], obj_id), # obj_id as tiebreaker
185
+ )
186
+ result[level] = sorted_objects
187
+
188
+ return result
189
+
190
+ def _calculate_positions(self) -> dict:
191
+ """Calculate optimized positions for all objects."""
192
+ connections = self._analyze_connections()
193
+ levels = self._calculate_flow_levels(connections)
194
+ groups = self._group_by_level(levels)
195
+
196
+ # Apply crossing minimization to reorder objects within each level
197
+ groups = self._minimize_crossings(groups, connections)
198
+
199
+ pad = self.pad
200
+
201
+ if self.flow_direction == "vertical":
202
+ return self._calculate_vertical_positions(groups, pad)
203
+ else:
204
+ return self._calculate_horizontal_positions(groups, pad)
205
+
206
+ def _calculate_horizontal_positions(self, groups: dict, pad: float) -> dict:
207
+ """Calculate positions for horizontal (left-to-right) flow."""
208
+ positions = {}
209
+ num_levels = max(len(groups), 1)
210
+
211
+ # Calculate available width per level
212
+ available_width = self.parent.width - 2 * pad
213
+ level_width = available_width / num_levels if groups else self.parent.width
214
+
215
+ for level, obj_ids in groups.items():
216
+ # Calculate x position based on level (left-to-right flow)
217
+ x_base = pad + (level * level_width * 0.8) # 0.8 factor for better spacing
218
+
219
+ # Calculate y positions for objects in this level
220
+ num_objects = len(obj_ids)
221
+ level_height = num_objects * (self.box_height + pad)
222
+
223
+ # Ensure y_start doesn't go negative - clamp to pad minimum
224
+ available_height = self.parent.height - 2 * pad
225
+ if level_height > available_height:
226
+ # Scale down spacing if too many objects
227
+ spacing = available_height / max(num_objects, 1)
228
+ y_start = pad
229
+ else:
230
+ spacing = self.box_height + pad
231
+ y_start = max(pad, (self.parent.height - level_height) / 2)
232
+
233
+ for i, obj_id in enumerate(obj_ids):
234
+ x = x_base
235
+ y = y_start + i * spacing
236
+
237
+ # Ensure positions stay within bounds
238
+ x = max(pad, min(x, self.parent.width - self.box_width - pad))
239
+ y = max(pad, min(y, self.parent.height - self.box_height - pad))
240
+
241
+ positions[obj_id] = Rect(x, y, self.box_width, self.box_height)
242
+
243
+ return positions
244
+
245
+ def _calculate_vertical_positions(self, groups: dict, pad: float) -> dict:
246
+ """Calculate positions for vertical (top-to-bottom) flow."""
247
+ positions = {}
248
+ num_levels = max(len(groups), 1)
249
+
250
+ # Calculate available height per level
251
+ available_height = self.parent.height - 2 * pad
252
+ level_height = available_height / num_levels if groups else self.parent.height
253
+
254
+ for level, obj_ids in groups.items():
255
+ # Calculate y position based on level (top-to-bottom flow)
256
+ y_base = pad + (level * level_height * 0.8) # 0.8 factor for better spacing
257
+
258
+ # Calculate x positions for objects in this level
259
+ num_objects = len(obj_ids)
260
+ level_width = num_objects * (self.box_width + pad)
261
+
262
+ # Ensure x_start doesn't go negative - clamp to pad minimum
263
+ available_width = self.parent.width - 2 * pad
264
+ if level_width > available_width:
265
+ # Scale down spacing if too many objects
266
+ spacing = available_width / max(num_objects, 1)
267
+ x_start = pad
268
+ else:
269
+ spacing = self.box_width + pad
270
+ x_start = max(pad, (self.parent.width - level_width) / 2)
271
+
272
+ for i, obj_id in enumerate(obj_ids):
273
+ y = y_base
274
+ x = x_start + i * spacing
275
+
276
+ # Ensure positions stay within bounds
277
+ x = max(pad, min(x, self.parent.width - self.box_width - pad))
278
+ y = max(pad, min(y, self.parent.height - self.box_height - pad))
279
+
280
+ positions[obj_id] = Rect(x, y, self.box_width, self.box_height)
281
+
282
+ return positions
283
+
284
+ def _get_object_position(self, obj_id: str) -> Rect:
285
+ """Get the calculated position for a specific object."""
286
+ if not self._position_cache:
287
+ self._position_cache = self._calculate_positions()
288
+
289
+ return self._position_cache.get(
290
+ obj_id, Rect(self.pad, self.pad, self.box_width, self.box_height)
291
+ )
292
+
293
+ def get_relative_pos(self, rect: Rect) -> Rect:
294
+ """Returns a flow-optimized position for the object."""
295
+ x, y, w, h = rect
296
+
297
+ # If we don't have enough information yet, fall back to simple grid
298
+ if len(self.parent._objects) <= 1:
299
+ pad = self.pad
300
+ x_shift = 3 * pad * self.x_layout_counter
301
+ y_shift = 1.5 * pad * self.y_layout_counter
302
+ x = pad + x_shift
303
+ y = pad + y_shift
304
+
305
+ self.x_layout_counter += 1
306
+ if x + w + 2 * pad > self.parent.width:
307
+ self.x_layout_counter = 0
308
+ self.y_layout_counter += 1
309
+
310
+ return Rect(x, y, w, h)
311
+
312
+ # For objects added after initial layout, try to maintain flow
313
+ # This is a simplified approach - in practice we'd want to recalculate
314
+ # the entire layout when significant changes occur
315
+ return self._get_next_flow_position(rect)
316
+
317
+ def _get_next_flow_position(self, rect: Rect) -> Rect:
318
+ """Calculate next position maintaining flow principles."""
319
+ x, y, w, h = rect
320
+ pad = self.pad
321
+
322
+ # Try to find a good position based on existing objects
323
+ existing_positions = [
324
+ (obj.patching_rect.x, obj.patching_rect.y)
325
+ for obj in self.parent._boxes
326
+ if hasattr(obj, "patching_rect")
327
+ ]
328
+
329
+ if existing_positions:
330
+ if self.flow_direction == "vertical":
331
+ # Find the bottommost object and place new object below it
332
+ max_y = max(pos[1] for pos in existing_positions)
333
+ avg_x = sum(pos[0] for pos in existing_positions) / len(
334
+ existing_positions
335
+ )
336
+
337
+ y = max_y + self.box_height + pad
338
+ x = avg_x
339
+
340
+ # Wrap if we exceed height
341
+ if y + h + pad > self.parent.height:
342
+ y = pad
343
+ x = max(pos[0] for pos in existing_positions) + self.box_width + pad
344
+ else:
345
+ # Find the rightmost object and place new object to its right
346
+ max_x = max(pos[0] for pos in existing_positions)
347
+ avg_y = sum(pos[1] for pos in existing_positions) / len(
348
+ existing_positions
349
+ )
350
+
351
+ x = max_x + self.box_width + pad
352
+ y = avg_y
353
+
354
+ # Wrap if we exceed width
355
+ if x + w + pad > self.parent.width:
356
+ x = pad
357
+ y = (
358
+ max(pos[1] for pos in existing_positions)
359
+ + self.box_height
360
+ + pad
361
+ )
362
+ else:
363
+ # First object - place at standard starting position
364
+ x = pad
365
+ y = pad
366
+
367
+ # Ensure positions stay within bounds
368
+ x = min(max(x, pad), self.parent.width - w - pad)
369
+ y = min(max(y, pad), self.parent.height - h - pad)
370
+
371
+ return Rect(x, y, w, h)
372
+
373
+ def optimize_layout(self, changed_objects: Optional[Set[str]] = None):
374
+ """Optimize the layout of all existing objects based on their connections.
375
+
376
+ Args:
377
+ changed_objects: Optional set of object IDs that have changed.
378
+ If provided and small relative to total objects, only
379
+ affected objects will be repositioned (incremental layout).
380
+ """
381
+ if len(self.parent._objects) < 2:
382
+ return # Nothing to optimize
383
+
384
+ # Use parent's incremental layout decision logic
385
+ if self.should_use_incremental(changed_objects):
386
+ assert changed_objects is not None
387
+ affected = self.get_affected_objects(changed_objects)
388
+ self._incremental_layout(affected)
389
+ else:
390
+ self._full_layout()
391
+
392
+ def _full_layout(self):
393
+ """Perform full layout optimization based on signal flow analysis."""
394
+ positions = self._calculate_positions()
395
+
396
+ # Apply optimized positions to existing objects
397
+ for obj_id, position in positions.items():
398
+ if obj_id in self.parent._objects:
399
+ self.parent._objects[obj_id].patching_rect = position
400
+
401
+ # Prevent any remaining overlaps after flow layout
402
+ self.prevent_overlaps()
403
+
404
+ # Clear cache so future positions use the optimized layout
405
+ self._position_cache = positions