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/grid.py ADDED
@@ -0,0 +1,374 @@
1
+ """Grid-based layout manager for py2max patches.
2
+
3
+ This module provides GridLayoutManager and legacy aliases for grid-based layouts.
4
+ """
5
+
6
+ from typing import Dict, 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 GridLayoutManager(LayoutManager):
15
+ """Utility class to help with object layout in a grid pattern.
16
+
17
+ This layout manager supports both horizontal and vertical grid layouts:
18
+ - Horizontal: objects fill from left to right and wrap to next row
19
+ - Vertical: objects fill from top to bottom and wrap to next column
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ parent: AbstractPatcher,
25
+ pad: Optional[int] = None,
26
+ box_width: Optional[int] = None,
27
+ box_height: Optional[int] = None,
28
+ comment_pad: Optional[int] = None,
29
+ flow_direction: str = "horizontal",
30
+ cluster_connected: bool = False,
31
+ ):
32
+ super().__init__(parent, pad, box_width, box_height, comment_pad)
33
+ self.flow_direction = flow_direction # "horizontal" or "vertical"
34
+ self.cluster_connected = (
35
+ cluster_connected # Whether to cluster connected objects
36
+ )
37
+
38
+ def get_relative_pos(self, rect: Rect) -> Rect:
39
+ """Returns a relative position for the object based on flow direction."""
40
+ # Always use simple grid positioning during object creation
41
+ # Clustering will be applied later via optimize_layout()
42
+ if self.flow_direction == "vertical":
43
+ return self._get_vertical_position(rect)
44
+ else:
45
+ return self._get_horizontal_position(rect)
46
+
47
+ def _get_horizontal_position(self, rect: Rect) -> Rect:
48
+ """Returns a relative horizontal position for the object.
49
+ Objects fill from left to right and wrap horizontally.
50
+ """
51
+ x, y, w, h = rect
52
+ pad = self.pad
53
+
54
+ x_shift = 3 * pad * self.x_layout_counter
55
+ y_shift = 1.5 * pad * self.y_layout_counter
56
+ x = pad + x_shift
57
+
58
+ self.x_layout_counter += 1
59
+ if x + w + 2 * pad > self.parent.width:
60
+ self.x_layout_counter = 0
61
+ self.y_layout_counter += 1
62
+
63
+ y = pad + y_shift
64
+ return Rect(x, y, w, h)
65
+
66
+ def _get_vertical_position(self, rect: Rect) -> Rect:
67
+ """Returns a relative vertical position for the object.
68
+ Objects fill from top to bottom and wrap vertically.
69
+ """
70
+ x, y, w, h = rect
71
+ pad = self.pad
72
+
73
+ x_shift = 3 * pad * self.x_layout_counter
74
+ y_shift = 1.5 * pad * self.y_layout_counter
75
+ y = pad + y_shift
76
+
77
+ self.y_layout_counter += 1
78
+ if y + h + 2 * pad > self.parent.height:
79
+ self.x_layout_counter += 1
80
+ self.y_layout_counter = 0
81
+
82
+ x = pad + x_shift
83
+ return Rect(x, y, w, h)
84
+
85
+ def _analyze_object_connections(self) -> dict:
86
+ """Analyze connections between objects to understand clustering patterns."""
87
+ connections: Dict[str, set] = {} # {obj_id: set(connected_obj_ids)}
88
+
89
+ # Initialize all objects with empty connection sets
90
+ for obj_id in self.parent._objects:
91
+ connections[obj_id] = set()
92
+
93
+ # Build bidirectional connection graph
94
+ for line in self.parent._lines:
95
+ src_id, dst_id = line.src, line.dst
96
+ # Skip if either ID is None
97
+ if src_id is None or dst_id is None:
98
+ continue
99
+ if src_id in connections and dst_id in connections:
100
+ connections[src_id].add(dst_id)
101
+ connections[dst_id].add(src_id)
102
+
103
+ return connections
104
+
105
+ def _find_connected_components(self, connections: dict) -> list[set]:
106
+ """Find clusters of connected objects using depth-first search."""
107
+ visited = set()
108
+ clusters = []
109
+
110
+ def dfs(obj_id, current_cluster):
111
+ if obj_id in visited:
112
+ return
113
+ visited.add(obj_id)
114
+ current_cluster.add(obj_id)
115
+
116
+ # Visit all connected objects
117
+ for connected_id in connections.get(obj_id, set()):
118
+ if connected_id not in visited:
119
+ dfs(connected_id, current_cluster)
120
+
121
+ # Find all connected components
122
+ for obj_id in connections:
123
+ if obj_id not in visited:
124
+ cluster: set[str] = set()
125
+ dfs(obj_id, cluster)
126
+ if cluster: # Only add non-empty clusters
127
+ clusters.append(cluster)
128
+
129
+ return clusters
130
+
131
+ def optimize_layout(self, changed_objects: Optional[Set[str]] = None):
132
+ """Optimize the layout to cluster connected objects together.
133
+
134
+ Args:
135
+ changed_objects: Optional set of object IDs that have changed.
136
+ If provided and small relative to total objects, only
137
+ affected objects will be repositioned (incremental layout).
138
+ """
139
+ # Use parent's incremental layout decision logic
140
+ if self.should_use_incremental(changed_objects):
141
+ assert changed_objects is not None
142
+ affected = self.get_affected_objects(changed_objects)
143
+ self._incremental_layout(affected)
144
+ else:
145
+ self._full_layout()
146
+
147
+ def _full_layout(self):
148
+ """Perform full layout optimization."""
149
+ if not self.cluster_connected or len(self.parent._objects) < 2:
150
+ # Even without clustering, prevent overlaps
151
+ self.prevent_overlaps()
152
+ return
153
+
154
+ # Analyze connections and create clusters
155
+ connections = self._analyze_object_connections()
156
+ clusters = self._find_connected_components(connections)
157
+
158
+ # Even single clusters can benefit from reorganization
159
+ # Apply optimized positions to existing objects based on clusters
160
+ self._apply_clustered_layout(clusters)
161
+
162
+ # Prevent any remaining overlaps after clustering
163
+ self.prevent_overlaps()
164
+
165
+ def _apply_clustered_layout(self, clusters: list[set]):
166
+ """Apply cluster-based positioning to all objects."""
167
+ # pad = self.pad
168
+
169
+ # If there's only one large cluster, try to create sub-clusters based on object types
170
+ if len(clusters) == 1 and len(clusters[0]) > 6:
171
+ clusters = self._subdivide_large_cluster(clusters[0])
172
+
173
+ # Sort clusters by size (largest first) for better space utilization
174
+ clusters = sorted(clusters, key=len, reverse=True)
175
+
176
+ # Use flow_direction to determine cluster arrangement
177
+ if self.flow_direction == "vertical":
178
+ self._apply_vertical_clustered_layout(clusters)
179
+ else:
180
+ self._apply_horizontal_clustered_layout(clusters)
181
+
182
+ def _apply_horizontal_clustered_layout(self, clusters: list[set]):
183
+ """Apply horizontal cluster-based positioning (clusters arranged left-to-right)."""
184
+ pad = self.pad
185
+ num_clusters = len(clusters)
186
+
187
+ # Calculate cluster layout for horizontal arrangement
188
+ if num_clusters <= 2:
189
+ cluster_cols = num_clusters
190
+ cluster_rows = 1
191
+ elif num_clusters <= 4:
192
+ cluster_cols = 2
193
+ cluster_rows = 2
194
+ else:
195
+ cluster_cols = min(3, num_clusters)
196
+ cluster_rows = (num_clusters + cluster_cols - 1) // cluster_cols
197
+
198
+ # Use float division for consistent spacing
199
+ cluster_width = (self.parent.width - 2 * pad) / max(cluster_cols, 1)
200
+ cluster_height = (self.parent.height - 2 * pad) / max(cluster_rows, 1)
201
+
202
+ # Consistent spacing between objects (use float)
203
+ object_spacing = pad * 0.5
204
+
205
+ # Position each cluster in its designated area
206
+ for cluster_idx, cluster_objects in enumerate(clusters):
207
+ cluster_objects_list = sorted(list(cluster_objects)) # Consistent ordering
208
+
209
+ # Calculate cluster's base position
210
+ cluster_col = cluster_idx % cluster_cols
211
+ cluster_row = cluster_idx // cluster_cols
212
+
213
+ cluster_x_base = cluster_col * cluster_width + pad
214
+ cluster_y_base = cluster_row * cluster_height + pad
215
+
216
+ # Position objects within this cluster's designated area (horizontal priority)
217
+ objects_per_row = max(1, int(cluster_width / (self.box_width + object_spacing)))
218
+
219
+ for obj_idx, obj_id in enumerate(cluster_objects_list):
220
+ if obj_id in self.parent._objects:
221
+ obj = self.parent._objects[obj_id]
222
+ if hasattr(obj, "patching_rect"):
223
+ # Calculate position within cluster (horizontal flow)
224
+ obj_col = obj_idx % objects_per_row
225
+ obj_row = obj_idx // objects_per_row
226
+
227
+ x = cluster_x_base + obj_col * (self.box_width + object_spacing)
228
+ y = cluster_y_base + obj_row * (self.box_height + object_spacing)
229
+
230
+ # Ensure bounds (stay within cluster area)
231
+ x = min(
232
+ max(x, cluster_x_base),
233
+ cluster_x_base + cluster_width - self.box_width - pad,
234
+ )
235
+ y = min(
236
+ max(y, cluster_y_base),
237
+ cluster_y_base + cluster_height - self.box_height - pad,
238
+ )
239
+
240
+ # Ensure overall patcher bounds
241
+ x = min(max(x, pad), self.parent.width - self.box_width - pad)
242
+ y = min(max(y, pad), self.parent.height - self.box_height - pad)
243
+
244
+ obj.patching_rect = Rect(x, y, self.box_width, self.box_height)
245
+
246
+ def _apply_vertical_clustered_layout(self, clusters: list[set]):
247
+ """Apply vertical cluster-based positioning (clusters arranged top-to-bottom)."""
248
+ pad = self.pad
249
+ num_clusters = len(clusters)
250
+
251
+ # Calculate cluster layout for vertical arrangement (prefer vertical stacking)
252
+ if num_clusters <= 2:
253
+ cluster_cols = 1
254
+ cluster_rows = num_clusters
255
+ elif num_clusters <= 4:
256
+ cluster_cols = 2
257
+ cluster_rows = 2
258
+ else:
259
+ cluster_rows = min(3, num_clusters)
260
+ cluster_cols = (num_clusters + cluster_rows - 1) // cluster_rows
261
+
262
+ # Use float division for consistent spacing
263
+ cluster_width = (self.parent.width - 2 * pad) / max(cluster_cols, 1)
264
+ cluster_height = (self.parent.height - 2 * pad) / max(cluster_rows, 1)
265
+
266
+ # Consistent spacing between objects (use float)
267
+ object_spacing = pad * 0.5
268
+
269
+ # Position each cluster in its designated area
270
+ for cluster_idx, cluster_objects in enumerate(clusters):
271
+ cluster_objects_list = sorted(list(cluster_objects)) # Consistent ordering
272
+
273
+ # Calculate cluster's base position (fill vertically first)
274
+ cluster_row = cluster_idx % cluster_rows
275
+ cluster_col = cluster_idx // cluster_rows
276
+
277
+ cluster_x_base = cluster_col * cluster_width + pad
278
+ cluster_y_base = cluster_row * cluster_height + pad
279
+
280
+ # Position objects within this cluster's designated area (vertical priority)
281
+ objects_per_col = max(
282
+ 1, int(cluster_height / (self.box_height + object_spacing))
283
+ )
284
+
285
+ for obj_idx, obj_id in enumerate(cluster_objects_list):
286
+ if obj_id in self.parent._objects:
287
+ obj = self.parent._objects[obj_id]
288
+ if hasattr(obj, "patching_rect"):
289
+ # Calculate position within cluster (vertical flow)
290
+ obj_row = obj_idx % objects_per_col
291
+ obj_col = obj_idx // objects_per_col
292
+
293
+ x = cluster_x_base + obj_col * (self.box_width + object_spacing)
294
+ y = cluster_y_base + obj_row * (self.box_height + object_spacing)
295
+
296
+ # Ensure bounds (stay within cluster area)
297
+ x = min(
298
+ max(x, cluster_x_base),
299
+ cluster_x_base + cluster_width - self.box_width - pad,
300
+ )
301
+ y = min(
302
+ max(y, cluster_y_base),
303
+ cluster_y_base + cluster_height - self.box_height - pad,
304
+ )
305
+
306
+ # Ensure overall patcher bounds
307
+ x = min(max(x, pad), self.parent.width - self.box_width - pad)
308
+ y = min(max(y, pad), self.parent.height - self.box_height - pad)
309
+
310
+ obj.patching_rect = Rect(x, y, self.box_width, self.box_height)
311
+
312
+ def _subdivide_large_cluster(self, cluster: set) -> list[set]:
313
+ """Subdivide a large cluster into smaller logical groups based on object types."""
314
+ # Group objects by type
315
+ type_groups: Dict[str, set[str]] = {}
316
+
317
+ for obj_id in cluster:
318
+ obj = self.parent._objects.get(obj_id)
319
+ if obj:
320
+ # Group by maxclass
321
+ obj_type = obj.maxclass
322
+ if obj_type not in type_groups:
323
+ type_groups[obj_type] = set()
324
+ type_groups[obj_type].add(obj_id)
325
+
326
+ # Convert type groups to clusters, combining small ones
327
+ subclusters = []
328
+ small_cluster = set()
329
+
330
+ for obj_type, obj_set in type_groups.items():
331
+ if len(obj_set) >= 3: # Large enough to be its own cluster
332
+ subclusters.append(obj_set)
333
+ else: # Too small, add to combined cluster
334
+ small_cluster.update(obj_set)
335
+
336
+ # Add the small objects cluster if it exists
337
+ if small_cluster:
338
+ subclusters.append(small_cluster)
339
+
340
+ # If we didn't create meaningful subdivisions, return original cluster
341
+ return subclusters if len(subclusters) > 1 else [cluster]
342
+
343
+
344
+ # Legacy aliases for backward compatibility
345
+ class HorizontalLayoutManager(GridLayoutManager):
346
+ """Legacy horizontal layout manager. Use GridLayoutManager with flow_direction="horizontal" instead."""
347
+
348
+ def __init__(
349
+ self,
350
+ parent: AbstractPatcher,
351
+ pad: Optional[int] = None,
352
+ box_width: Optional[int] = None,
353
+ box_height: Optional[int] = None,
354
+ comment_pad: Optional[int] = None,
355
+ ):
356
+ super().__init__(
357
+ parent, pad, box_width, box_height, comment_pad, flow_direction="horizontal"
358
+ )
359
+
360
+
361
+ class VerticalLayoutManager(GridLayoutManager):
362
+ """Legacy vertical layout manager. Use GridLayoutManager with flow_direction="vertical" instead."""
363
+
364
+ def __init__(
365
+ self,
366
+ parent: AbstractPatcher,
367
+ pad: Optional[int] = None,
368
+ box_width: Optional[int] = None,
369
+ box_height: Optional[int] = None,
370
+ comment_pad: Optional[int] = None,
371
+ ):
372
+ super().__init__(
373
+ parent, pad, box_width, box_height, comment_pad, flow_direction="vertical"
374
+ )