py2max 0.3.4__tar.gz → 0.3.6__tar.gz

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 (179) hide show
  1. {py2max-0.3.4 → py2max-0.3.6}/CHANGELOG.md +14 -0
  2. {py2max-0.3.4 → py2max-0.3.6}/PKG-INFO +1 -1
  3. {py2max-0.3.4 → py2max-0.3.6}/py2max/__init__.py +1 -1
  4. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/patcher.py +8 -6
  5. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/base.py +10 -195
  6. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/flow.py +2 -18
  7. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/grid.py +0 -16
  8. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/matrix.py +5 -6
  9. {py2max-0.3.4 → py2max-0.3.6}/pyproject.toml +1 -1
  10. {py2max-0.3.4 → py2max-0.3.6}/tests/test_patcher.py +30 -0
  11. {py2max-0.3.4 → py2max-0.3.6}/LICENSE +0 -0
  12. {py2max-0.3.4 → py2max-0.3.6}/README.md +0 -0
  13. {py2max-0.3.4 → py2max-0.3.6}/py2max/__main__.py +0 -0
  14. {py2max-0.3.4 → py2max-0.3.6}/py2max/cli.py +0 -0
  15. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/__init__.py +0 -0
  16. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/abstract.py +0 -0
  17. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/box.py +0 -0
  18. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/colors.py +0 -0
  19. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/common.py +0 -0
  20. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/factory.py +0 -0
  21. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/patchline.py +0 -0
  22. {py2max-0.3.4 → py2max-0.3.6}/py2max/core/serialization.py +0 -0
  23. {py2max-0.3.4 → py2max-0.3.6}/py2max/exceptions.py +0 -0
  24. {py2max-0.3.4 → py2max-0.3.6}/py2max/export/__init__.py +0 -0
  25. {py2max-0.3.4 → py2max-0.3.6}/py2max/export/converters.py +0 -0
  26. {py2max-0.3.4 → py2max-0.3.6}/py2max/export/svg.py +0 -0
  27. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/__init__.py +0 -0
  28. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/external.py +0 -0
  29. {py2max-0.3.4 → py2max-0.3.6}/py2max/layout/graph.py +0 -0
  30. {py2max-0.3.4 → py2max-0.3.6}/py2max/lint.py +0 -0
  31. {py2max-0.3.4 → py2max-0.3.6}/py2max/log.py +0 -0
  32. {py2max-0.3.4 → py2max-0.3.6}/py2max/m4l.py +0 -0
  33. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/__init__.py +0 -0
  34. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/category.py +0 -0
  35. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/data/bundle.json.gz +0 -0
  36. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/db.py +0 -0
  37. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/legacy.py +0 -0
  38. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/parser.py +0 -0
  39. {py2max-0.3.4 → py2max-0.3.6}/py2max/maxref/porttypes.py +0 -0
  40. {py2max-0.3.4 → py2max-0.3.6}/py2max/py.typed +0 -0
  41. {py2max-0.3.4 → py2max-0.3.6}/py2max/transformers.py +0 -0
  42. {py2max-0.3.4 → py2max-0.3.6}/py2max/utils.py +0 -0
  43. {py2max-0.3.4 → py2max-0.3.6}/tests/__init__.py +0 -0
  44. {py2max-0.3.4 → py2max-0.3.6}/tests/conftest.py +0 -0
  45. {py2max-0.3.4 → py2max-0.3.6}/tests/data/complex.maxpat +0 -0
  46. {py2max-0.3.4 → py2max-0.3.6}/tests/data/desc.maxpat +0 -0
  47. {py2max-0.3.4 → py2max-0.3.6}/tests/data/empty.maxpat +0 -0
  48. {py2max-0.3.4 → py2max-0.3.6}/tests/data/mydevice.amxd +0 -0
  49. {py2max-0.3.4 → py2max-0.3.6}/tests/data/mydevice2.amxd +0 -0
  50. {py2max-0.3.4 → py2max-0.3.6}/tests/data/nested.maxpat +0 -0
  51. {py2max-0.3.4 → py2max-0.3.6}/tests/data/simple.maxpat +0 -0
  52. {py2max-0.3.4 → py2max-0.3.6}/tests/data/tabular.maxpat +0 -0
  53. {py2max-0.3.4 → py2max-0.3.6}/tests/data/umenu.maxref.xml +0 -0
  54. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/README.md +0 -0
  55. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/advanced/connection_patterns.py +0 -0
  56. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/advanced/custom_extensions.py +0 -0
  57. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/advanced/data_containers.py +0 -0
  58. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/advanced/error_handling.py +0 -0
  59. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/advanced/performance_optimization.py +0 -0
  60. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/advanced/subpatchers.py +0 -0
  61. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/api/box_api_examples.py +0 -0
  62. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/api/patcher_api_examples.py +0 -0
  63. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/auto_layout_demo.py +0 -0
  64. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/db/category_db_demo.py +0 -0
  65. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/db/maxref_db_demo.py +0 -0
  66. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/layout/columnar_layout_examples.py +0 -0
  67. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/layout/flow_layout_examples.py +0 -0
  68. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/layout/grid_layout_examples.py +0 -0
  69. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/layout/matrix_layout_examples.py +0 -0
  70. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/basic_synth.maxpat +0 -0
  71. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/basic_synth.svg +0 -0
  72. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/complex_synth.maxpat +0 -0
  73. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/complex_synth.svg +0 -0
  74. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/flow_layout.maxpat +0 -0
  75. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/flow_layout.svg +0 -0
  76. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/grid_layout.maxpat +0 -0
  77. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/grid_layout.svg +0 -0
  78. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  79. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/horizontal_layout.svg +0 -0
  80. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/styled_no_ports.svg +0 -0
  81. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/styled_no_title.svg +0 -0
  82. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/styled_patch.maxpat +0 -0
  83. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/styled_with_ports.svg +0 -0
  84. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/svg_preview_demo.py +0 -0
  85. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/vertical_layout.maxpat +0 -0
  86. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/vertical_layout.svg +0 -0
  87. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/workflow_demo.maxpat +0 -0
  88. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/preview/workflow_demo.svg +0 -0
  89. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/quickstart/basic_patch.py +0 -0
  90. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/quickstart/layout_examples.py +0 -0
  91. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/tutorial/generative_music.py +0 -0
  92. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/tutorial/interactive_controller.py +0 -0
  93. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  94. {py2max-0.3.4 → py2max-0.3.6}/tests/examples/tutorial/simple_synthesis.py +0 -0
  95. {py2max-0.3.4 → py2max-0.3.6}/tests/graphs/random/v30e33.tglf +0 -0
  96. {py2max-0.3.4 → py2max-0.3.6}/tests/test_abstract_coverage.py +0 -0
  97. {py2max-0.3.4 → py2max-0.3.6}/tests/test_abstraction.py +0 -0
  98. {py2max-0.3.4 → py2max-0.3.6}/tests/test_add.py +0 -0
  99. {py2max-0.3.4 → py2max-0.3.6}/tests/test_amxd.py +0 -0
  100. {py2max-0.3.4 → py2max-0.3.6}/tests/test_attrui.py +0 -0
  101. {py2max-0.3.4 → py2max-0.3.6}/tests/test_basic.py +0 -0
  102. {py2max-0.3.4 → py2max-0.3.6}/tests/test_beap.py +0 -0
  103. {py2max-0.3.4 → py2max-0.3.6}/tests/test_bpatcher.py +0 -0
  104. {py2max-0.3.4 → py2max-0.3.6}/tests/test_cli.py +0 -0
  105. {py2max-0.3.4 → py2max-0.3.6}/tests/test_coll.py +0 -0
  106. {py2max-0.3.4 → py2max-0.3.6}/tests/test_colors.py +0 -0
  107. {py2max-0.3.4 → py2max-0.3.6}/tests/test_colors_theme.py +0 -0
  108. {py2max-0.3.4 → py2max-0.3.6}/tests/test_comment.py +0 -0
  109. {py2max-0.3.4 → py2max-0.3.6}/tests/test_connection_validation.py +0 -0
  110. {py2max-0.3.4 → py2max-0.3.6}/tests/test_converters.py +0 -0
  111. {py2max-0.3.4 → py2max-0.3.6}/tests/test_core_coverage.py +0 -0
  112. {py2max-0.3.4 → py2max-0.3.6}/tests/test_db.py +0 -0
  113. {py2max-0.3.4 → py2max-0.3.6}/tests/test_defaults.py +0 -0
  114. {py2max-0.3.4 → py2max-0.3.6}/tests/test_dict.py +0 -0
  115. {py2max-0.3.4 → py2max-0.3.6}/tests/test_edit_operations.py +0 -0
  116. {py2max-0.3.4 → py2max-0.3.6}/tests/test_encapsulate.py +0 -0
  117. {py2max-0.3.4 → py2max-0.3.6}/tests/test_error_handling.py +0 -0
  118. {py2max-0.3.4 → py2max-0.3.6}/tests/test_examples.py +0 -0
  119. {py2max-0.3.4 → py2max-0.3.6}/tests/test_ezdac.py +0 -0
  120. {py2max-0.3.4 → py2max-0.3.6}/tests/test_gen.py +0 -0
  121. {py2max-0.3.4 → py2max-0.3.6}/tests/test_group.py +0 -0
  122. {py2max-0.3.4 → py2max-0.3.6}/tests/test_itable.py +0 -0
  123. {py2max-0.3.4 → py2max-0.3.6}/tests/test_js.py +0 -0
  124. {py2max-0.3.4 → py2max-0.3.6}/tests/test_kwds_filter.py +0 -0
  125. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout.py +0 -0
  126. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_box_dims.py +0 -0
  127. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_builtins.py +0 -0
  128. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_coverage.py +0 -0
  129. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_flow.py +0 -0
  130. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_graph.py +0 -0
  131. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_graph_layout.py +0 -0
  132. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_graph_manager.py +0 -0
  133. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_matrix.py +0 -0
  134. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_overlaps.py +0 -0
  135. {py2max-0.3.4 → py2max-0.3.6}/tests/test_layout_vertical.py +0 -0
  136. {py2max-0.3.4 → py2max-0.3.6}/tests/test_linking.py +0 -0
  137. {py2max-0.3.4 → py2max-0.3.6}/tests/test_lint.py +0 -0
  138. {py2max-0.3.4 → py2max-0.3.6}/tests/test_m4l.py +0 -0
  139. {py2max-0.3.4 → py2max-0.3.6}/tests/test_maxref.py +0 -0
  140. {py2max-0.3.4 → py2max-0.3.6}/tests/test_maxref_bundle.py +0 -0
  141. {py2max-0.3.4 → py2max-0.3.6}/tests/test_maxref_lazy.py +0 -0
  142. {py2max-0.3.4 → py2max-0.3.6}/tests/test_maxref_parser.py +0 -0
  143. {py2max-0.3.4 → py2max-0.3.6}/tests/test_maxref_refpages.py +0 -0
  144. {py2max-0.3.4 → py2max-0.3.6}/tests/test_mc_cycle.py +0 -0
  145. {py2max-0.3.4 → py2max-0.3.6}/tests/test_mc_poly.py +0 -0
  146. {py2max-0.3.4 → py2max-0.3.6}/tests/test_message.py +0 -0
  147. {py2max-0.3.4 → py2max-0.3.6}/tests/test_mypatch.py +0 -0
  148. {py2max-0.3.4 → py2max-0.3.6}/tests/test_nested.py +0 -0
  149. {py2max-0.3.4 → py2max-0.3.6}/tests/test_nested_patchers.py +0 -0
  150. {py2max-0.3.4 → py2max-0.3.6}/tests/test_number_tilde.py +0 -0
  151. {py2max-0.3.4 → py2max-0.3.6}/tests/test_numbers.py +0 -0
  152. {py2max-0.3.4 → py2max-0.3.6}/tests/test_param.py +0 -0
  153. {py2max-0.3.4 → py2max-0.3.6}/tests/test_param_placement.py +0 -0
  154. {py2max-0.3.4 → py2max-0.3.6}/tests/test_pitched_osc.py +0 -0
  155. {py2max-0.3.4 → py2max-0.3.6}/tests/test_porttypes.py +0 -0
  156. {py2max-0.3.4 → py2max-0.3.6}/tests/test_presets.py +0 -0
  157. {py2max-0.3.4 → py2max-0.3.6}/tests/test_pydantic.py +0 -0
  158. {py2max-0.3.4 → py2max-0.3.6}/tests/test_rnbo.py +0 -0
  159. {py2max-0.3.4 → py2max-0.3.6}/tests/test_rnbo_subpatcher.py +0 -0
  160. {py2max-0.3.4 → py2max-0.3.6}/tests/test_scripting_name.py +0 -0
  161. {py2max-0.3.4 → py2max-0.3.6}/tests/test_search.py +0 -0
  162. {py2max-0.3.4 → py2max-0.3.6}/tests/test_semantic_ids.py +0 -0
  163. {py2max-0.3.4 → py2max-0.3.6}/tests/test_subpatch.py +0 -0
  164. {py2max-0.3.4 → py2max-0.3.6}/tests/test_svg.py +0 -0
  165. {py2max-0.3.4 → py2max-0.3.6}/tests/test_svg_fidelity.py +0 -0
  166. {py2max-0.3.4 → py2max-0.3.6}/tests/test_table.py +0 -0
  167. {py2max-0.3.4 → py2max-0.3.6}/tests/test_transformers.py +0 -0
  168. {py2max-0.3.4 → py2max-0.3.6}/tests/test_transformers_e4.py +0 -0
  169. {py2max-0.3.4 → py2max-0.3.6}/tests/test_tree.py +0 -0
  170. {py2max-0.3.4 → py2max-0.3.6}/tests/test_tree_builder.py +0 -0
  171. {py2max-0.3.4 → py2max-0.3.6}/tests/test_tutorial_simple_synthesis.py +0 -0
  172. {py2max-0.3.4 → py2max-0.3.6}/tests/test_two_sines.py +0 -0
  173. {py2max-0.3.4 → py2max-0.3.6}/tests/test_umenu.py +0 -0
  174. {py2max-0.3.4 → py2max-0.3.6}/tests/test_utils.py +0 -0
  175. {py2max-0.3.4 → py2max-0.3.6}/tests/test_validate_attrs.py +0 -0
  176. {py2max-0.3.4 → py2max-0.3.6}/tests/test_validation.py +0 -0
  177. {py2max-0.3.4 → py2max-0.3.6}/tests/test_varname.py +0 -0
  178. {py2max-0.3.4 → py2max-0.3.6}/tests/test_wheel_bundle.py +0 -0
  179. {py2max-0.3.4 → py2max-0.3.6}/tests/test_zl_group.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.6]
6
+
7
+ ### Removed: incremental layout; `optimize_layout()` is batch-only again
8
+
9
+ - `Patcher.optimize_layout()` no longer takes the `changed_objects` parameter added in 0.3.5 -- it takes no arguments and always performs a full, whole-patch layout. The incremental machinery in the layout managers was removed with it: `LayoutManager.should_use_incremental`, `get_affected_objects`, `get_connected_objects`, `_incremental_layout`, `_find_non_overlapping_position`, and the `INCREMENTAL_THRESHOLD` constant (`layout/base.py`); the `optimize_layout(changed_objects)` overrides in `layout/grid.py` and `layout/flow.py` (they now implement `_full_layout` and inherit the batch entry point, with flow's `<2 objects` guard moved into `_full_layout`); and `layout/matrix.py`'s override (now `_full_layout`, with `ColumnarLayoutManager` inheriting it).
10
+ - **Rationale (scope split):** py2max owns *batch* layout -- arranging a whole patch once, typically at the end of programmatic creation. Interactive, per-edit ("live") relayout belongs to the editor that owns the editing session (`py2max-server`), which handles it client-side. The incremental path existed only to serve that live case and was never exercised by a batch caller (batch `optimize_layout()` always passed `changed_objects=None`), so it was dead weight in the library. This reverses the 0.3.5 change, which had added the parameter as a prerequisite for a server-side auto-layout approach that was subsequently dropped.
11
+ - **Breaking:** calling `optimize_layout()` with an argument (e.g. `optimize_layout({obj.id})`) now raises `TypeError`; drop the argument. The `test_optimize_layout_forwards_changed_objects` / `..._incremental_leaves_untouched_objects_fixed` regression tests were replaced by `test_optimize_layout_is_batch_only`.
12
+
13
+ ## [0.3.5]
14
+
15
+ ### Fixed: `Patcher.optimize_layout()` now reaches the incremental layout path
16
+
17
+ - `Patcher.optimize_layout()` gained an optional `changed_objects: Optional[Set[str]]` parameter and forwards it to the layout manager. It previously called the manager with no arguments, silently discarding any change set, so the incremental layout engine (`layout/base.py` `optimize_layout(changed_objects)` / `should_use_incremental` / `_incremental_layout`) was unreachable through the public API -- every call forced a full relayout. Passing a set of object IDs now lets managers that support it (grid, flow) reposition only those objects and their patchline neighbours; `None` (the default) preserves the previous full-relayout behaviour, so the change is backward compatible. Matrix/columnar managers still recompute in full (they ignore the argument by design). This is the core prerequisite for server-side auto-layout, which lives in `py2max-server`.
18
+
5
19
  ## [0.3.4]
6
20
 
7
21
  ### New: Param docking in layouts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py2max
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: A library for offline generation of Max/MSP patcher (.maxpat) files.
5
5
  Keywords: Max,maxpat,offline
6
6
  Author: Shakeeb Alireza
@@ -35,7 +35,7 @@ Example:
35
35
  >>> p.save()
36
36
  """
37
37
 
38
- __version__ = "0.3.4"
38
+ __version__ = "0.3.6"
39
39
 
40
40
  from .core import Box, Patcher, Patchline
41
41
  from .exceptions import (
@@ -657,18 +657,20 @@ class Patcher(BoxFactoryMixin, SerializationMixin, AbstractPatcher):
657
657
  return self._layout_mgr.get_pos()
658
658
 
659
659
  def optimize_layout(self) -> None:
660
- """Optimize object positions based on layout manager type.
660
+ """Arrange the whole patch based on the active layout manager.
661
661
 
662
- Calls the layout manager's optimization method to improve object
663
- positioning, then repositions any associated comments based on
664
- the new box positions. The effect depends on the layout manager:
662
+ Calls the layout manager to lay out every object, then repositions any
663
+ associated comments based on the new box positions. The effect depends
664
+ on the layout manager:
665
665
 
666
666
  - FlowLayoutManager: Arranges objects by signal flow topology
667
667
  - GridLayoutManager: Clusters connected objects together
668
668
  - Other managers: May have limited or no effect
669
669
 
670
- This method should be called after all objects and connections
671
- have been added to the patch.
670
+ This is a batch, whole-patch operation intended to be called once after
671
+ all objects and connections have been added. Interactive, per-edit
672
+ relayout is out of scope for py2max (see ``docs/auto-layout.md`` in
673
+ py2max-server).
672
674
  """
673
675
  if hasattr(self._layout_mgr, "optimize_layout"):
674
676
  self._layout_mgr.optimize_layout()
@@ -3,17 +3,12 @@
3
3
  This module provides the base LayoutManager class that all layout managers inherit from.
4
4
  """
5
5
 
6
- from typing import Any, Dict, List, Optional, Set, Tuple, cast
6
+ from typing import Any, Dict, List, Optional, Tuple, cast
7
7
 
8
8
  from py2max.core.abstract import AbstractLayoutManager, AbstractPatcher
9
9
  from py2max.core.common import Rect
10
10
  from py2max.maxref import MAXCLASS_DEFAULTS
11
11
 
12
- from .graph import PatchGraph
13
-
14
- # Threshold for incremental vs full layout (30% of total objects)
15
- INCREMENTAL_THRESHOLD = 0.3
16
-
17
12
  # Value/UI "param" objects: their role is to set a value on one object's inlet,
18
13
  # so they read better docked next to that object than spread through the graph.
19
14
  PARAM_MAXCLASSES = frozenset(
@@ -358,81 +353,18 @@ class LayoutManager(AbstractLayoutManager):
358
353
  return tx + tw / 2
359
354
  return tx + tw * min(inlet, n_inlets - 1) / (n_inlets - 1)
360
355
 
361
- def get_connected_objects(self, obj_ids: Set[str]) -> Set[str]:
362
- """Get all objects connected to the given objects via patchlines.
363
-
364
- Args:
365
- obj_ids: Set of object IDs to find connections for.
366
-
367
- Returns:
368
- Set of object IDs that are directly connected to the input objects.
369
- """
370
- return PatchGraph(self.parent._lines).neighbors_of(obj_ids)
371
-
372
- def get_affected_objects(self, changed_objects: Set[str]) -> Set[str]:
373
- """Get all objects affected by changes to the given objects.
374
-
375
- This includes the changed objects themselves plus their immediate
376
- neighbors (objects connected via patchlines).
377
-
378
- Args:
379
- changed_objects: Set of object IDs that have changed.
380
-
381
- Returns:
382
- Set of all affected object IDs.
383
- """
384
- affected = set(changed_objects)
385
- neighbors = self.get_connected_objects(changed_objects)
386
- affected.update(neighbors)
387
- return affected
388
-
389
- def should_use_incremental(self, changed_objects: Optional[Set[str]]) -> bool:
390
- """Determine whether to use incremental or full layout.
391
-
392
- Uses incremental layout when:
393
- - changed_objects is provided
394
- - The number of affected objects is less than INCREMENTAL_THRESHOLD
395
- of total objects
396
-
397
- Args:
398
- changed_objects: Set of changed object IDs, or None for full layout.
399
-
400
- Returns:
401
- True if incremental layout should be used.
402
- """
403
- if changed_objects is None:
404
- return False
405
-
406
- total_objects = len(self.parent._objects)
407
- if total_objects == 0:
408
- return False
409
-
410
- affected = self.get_affected_objects(changed_objects)
411
- return len(affected) < total_objects * INCREMENTAL_THRESHOLD
356
+ def optimize_layout(self) -> None:
357
+ """Arrange every object in the patch (batch, whole-patch layout).
412
358
 
413
- def optimize_layout(self, changed_objects: Optional[Set[str]] = None) -> None:
414
- """Optimize the layout of objects.
359
+ This is py2max's batch layout entry point: it always performs a full
360
+ layout of the entire patch. Subclasses implement their algorithm in
361
+ ``_full_layout()``.
415
362
 
416
- If changed_objects is provided and the number of affected objects
417
- is small relative to the total, only those objects and their
418
- neighbors will be repositioned (incremental layout). Otherwise,
419
- a full layout recalculation is performed.
420
-
421
- Args:
422
- changed_objects: Optional set of object IDs that have changed.
423
- If None, performs full layout optimization.
424
-
425
- Subclasses should override _full_layout() and optionally
426
- _incremental_layout() to implement specific algorithms.
363
+ Interactive, per-edit ("incremental") relayout is intentionally out of
364
+ scope for py2max; it belongs to the editor/server that owns the live
365
+ editing session. See ``docs/auto-layout.md`` in py2max-server.
427
366
  """
428
- if self.should_use_incremental(changed_objects):
429
- # changed_objects is guaranteed non-None here since should_use_incremental
430
- # returns False when changed_objects is None
431
- assert changed_objects is not None
432
- affected = self.get_affected_objects(changed_objects)
433
- self._incremental_layout(affected)
434
- else:
435
- self._full_layout()
367
+ self._full_layout()
436
368
 
437
369
  def _full_layout(self) -> None:
438
370
  """Perform full layout optimization.
@@ -441,120 +373,3 @@ class LayoutManager(AbstractLayoutManager):
441
373
  full layout algorithm.
442
374
  """
443
375
  pass
444
-
445
- def _incremental_layout(self, affected_objects: Set[str]) -> None:
446
- """Perform incremental layout optimization for affected objects.
447
-
448
- This method repositions only the affected objects while keeping
449
- other objects in their current positions. The default implementation
450
- finds optimal positions for affected objects that minimize overlap
451
- with existing objects.
452
-
453
- Args:
454
- affected_objects: Set of object IDs to reposition.
455
-
456
- Subclasses can override this for more sophisticated incremental layouts.
457
- """
458
- if not affected_objects:
459
- return
460
-
461
- # Get current positions of non-affected objects (these are fixed)
462
- fixed_positions: Dict[str, Rect] = {}
463
- for obj_id, obj in self.parent._objects.items():
464
- if obj_id not in affected_objects:
465
- if hasattr(obj, "patching_rect"):
466
- fixed_positions[obj_id] = obj.patching_rect
467
-
468
- # For each affected object, find a position that doesn't overlap
469
- for obj_id in affected_objects:
470
- if obj_id not in self.parent._objects:
471
- continue
472
-
473
- obj = self.parent._objects[obj_id]
474
- if not hasattr(obj, "patching_rect"):
475
- continue
476
-
477
- current_rect = obj.patching_rect
478
-
479
- # Try to find a nearby position that doesn't overlap
480
- best_pos = self._find_non_overlapping_position(
481
- current_rect, fixed_positions, affected_objects
482
- )
483
-
484
- if best_pos:
485
- obj.patching_rect = best_pos
486
- # Add to fixed positions for subsequent objects
487
- fixed_positions[obj_id] = best_pos
488
-
489
- # Final overlap prevention pass
490
- self.prevent_overlaps()
491
-
492
- def _find_non_overlapping_position(
493
- self,
494
- rect: Rect,
495
- fixed_positions: Dict[str, Rect],
496
- exclude_ids: Set[str],
497
- ) -> Optional[Rect]:
498
- """Find a position for rect that doesn't overlap with fixed positions.
499
-
500
- Tries positions in a spiral pattern around the current position.
501
-
502
- Args:
503
- rect: The rectangle to position.
504
- fixed_positions: Dictionary of fixed object positions.
505
- exclude_ids: Object IDs to exclude from overlap checking.
506
-
507
- Returns:
508
- A non-overlapping Rect, or the original rect if no better position found.
509
- """
510
- pad = self.pad
511
- min_gap = 10.0
512
-
513
- def overlaps_any(test_rect: Rect) -> bool:
514
- for obj_id, fixed_rect in fixed_positions.items():
515
- if obj_id in exclude_ids:
516
- continue
517
- # Check overlap
518
- if (
519
- test_rect.x < fixed_rect.x + fixed_rect.w + min_gap
520
- and fixed_rect.x < test_rect.x + test_rect.w + min_gap
521
- and test_rect.y < fixed_rect.y + fixed_rect.h + min_gap
522
- and fixed_rect.y < test_rect.y + test_rect.h + min_gap
523
- ):
524
- return True
525
- return False
526
-
527
- # If current position doesn't overlap, keep it
528
- if not overlaps_any(rect):
529
- return rect
530
-
531
- # Try positions in a spiral pattern
532
- step = pad / 2
533
- max_radius = max(self.parent.width, self.parent.height) / 2
534
-
535
- for radius in range(1, int(max_radius / step) + 1):
536
- offset = radius * step
537
- # Try 8 directions
538
- for dx, dy in [
539
- (offset, 0),
540
- (-offset, 0),
541
- (0, offset),
542
- (0, -offset),
543
- (offset, offset),
544
- (-offset, offset),
545
- (offset, -offset),
546
- (-offset, -offset),
547
- ]:
548
- new_x = rect.x + dx
549
- new_y = rect.y + dy
550
-
551
- # Ensure within bounds
552
- new_x = max(pad, min(new_x, self.parent.width - rect.w - pad))
553
- new_y = max(pad, min(new_y, self.parent.height - rect.h - pad))
554
-
555
- test_rect = Rect(new_x, new_y, rect.w, rect.h)
556
- if not overlaps_any(test_rect):
557
- return test_rect
558
-
559
- # No non-overlapping position found, return original
560
- return rect
@@ -365,27 +365,11 @@ class FlowLayoutManager(LayoutManager):
365
365
 
366
366
  return Rect(x, y, w, h)
367
367
 
368
- def optimize_layout(self, changed_objects: Optional[Set[str]] = None) -> None:
369
- """Optimize the layout of all existing objects based on their connections.
370
-
371
- Args:
372
- changed_objects: Optional set of object IDs that have changed.
373
- If provided and small relative to total objects, only
374
- affected objects will be repositioned (incremental layout).
375
- """
368
+ def _full_layout(self) -> None:
369
+ """Perform full layout optimization based on signal flow analysis."""
376
370
  if len(self.parent._objects) < 2:
377
371
  return # Nothing to optimize
378
372
 
379
- # Use parent's incremental layout decision logic
380
- if self.should_use_incremental(changed_objects):
381
- assert changed_objects is not None
382
- affected = self.get_affected_objects(changed_objects)
383
- self._incremental_layout(affected)
384
- else:
385
- self._full_layout()
386
-
387
- def _full_layout(self) -> None:
388
- """Perform full layout optimization based on signal flow analysis."""
389
373
  positions = self._calculate_positions()
390
374
 
391
375
  # Apply optimized positions to existing objects. The computed position
@@ -83,22 +83,6 @@ class GridLayoutManager(LayoutManager):
83
83
  x = pad + x_shift
84
84
  return Rect(x, y, w, h)
85
85
 
86
- def optimize_layout(self, changed_objects: Optional[Set[str]] = None) -> None:
87
- """Optimize the layout to cluster connected objects together.
88
-
89
- Args:
90
- changed_objects: Optional set of object IDs that have changed.
91
- If provided and small relative to total objects, only
92
- affected objects will be repositioned (incremental layout).
93
- """
94
- # Use parent's incremental layout decision logic
95
- if self.should_use_incremental(changed_objects):
96
- assert changed_objects is not None
97
- affected = self.get_affected_objects(changed_objects)
98
- self._incremental_layout(affected)
99
- else:
100
- self._full_layout()
101
-
102
86
  def _full_layout(self) -> None:
103
87
  """Perform full layout optimization."""
104
88
  if not self.cluster_connected or len(self.parent._objects) < 2:
@@ -3,7 +3,7 @@
3
3
  This module provides MatrixLayoutManager for matrix and columnar-based layouts.
4
4
  """
5
5
 
6
- from typing import Any, Dict, List, Optional, Set, Tuple
6
+ from typing import Any, Dict, List, Optional, Tuple
7
7
 
8
8
  from py2max.core.abstract import AbstractBox, AbstractPatcher
9
9
  from py2max.core.common import Rect
@@ -272,12 +272,11 @@ class MatrixLayoutManager(LayoutManager):
272
272
 
273
273
  return Rect(x, y, w, h)
274
274
 
275
- def optimize_layout(self, changed_objects: Optional[Set[str]] = None) -> None:
276
- """Optimize the layout based on flow_direction setting.
275
+ def _full_layout(self) -> None:
276
+ """Arrange objects based on the ``flow_direction`` setting.
277
277
 
278
- ``changed_objects`` is accepted for interface compatibility with
279
- ``LayoutManager`` but ignored: the matrix layout always recomputes the
280
- full arrangement from the signal-chain analysis.
278
+ The matrix layout always recomputes the full arrangement from the
279
+ signal-chain analysis.
281
280
  """
282
281
  if len(self.parent._objects) < 1:
283
282
  return
@@ -7,7 +7,7 @@ authors = [
7
7
  maintainers = [
8
8
  { name = "Shakeeb Alireza", email = "shakfu@users.noreply.github.com" }
9
9
  ]
10
- version = "0.3.4"
10
+ version = "0.3.6"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"
13
13
  keywords = ["Max", "maxpat", "offline"]
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
 
3
3
  from py2max import Patcher
4
4
  import json
5
+ import pytest
5
6
 
6
7
  DATA_DIR = Path(__file__).parent / "data"
7
8
 
@@ -33,6 +34,35 @@ def test_patcher_from_file():
33
34
  p.save_as("outputs/test_complex2.maxpat")
34
35
 
35
36
 
37
+ def test_optimize_layout_is_batch_only():
38
+ """``optimize_layout`` is a batch, whole-patch operation that takes no
39
+ arguments.
40
+
41
+ Interactive, per-edit ("incremental") relayout was intentionally removed
42
+ from py2max; it belongs to the editor/server that owns the live editing
43
+ session (see docs/auto-layout.md in py2max-server). Passing a change set is
44
+ therefore a TypeError, at both the facade and the layout-manager level.
45
+ """
46
+ p = Patcher("outputs/test_optimize_batch_only.maxpat", layout="grid")
47
+ a = p.add_textbox("cycle~ 440")
48
+ b = p.add_textbox("gain~")
49
+ p.add_line(a, b)
50
+
51
+ # The batch call arranges every object and must not raise.
52
+ p.optimize_layout()
53
+ assert a.patching_rect and b.patching_rect
54
+
55
+ # The removed incremental API is gone at both levels.
56
+ with pytest.raises(TypeError):
57
+ p.optimize_layout({a.id}) # type: ignore[call-arg]
58
+ with pytest.raises(TypeError):
59
+ p._layout_mgr.optimize_layout({a.id}) # type: ignore[call-arg]
60
+
61
+ # The incremental helpers no longer exist on the layout manager.
62
+ assert not hasattr(p._layout_mgr, "should_use_incremental")
63
+ assert not hasattr(p._layout_mgr, "_incremental_layout")
64
+
65
+
36
66
  def test_patcher_from_file_comparison_complex():
37
67
  pd = Patcher.from_file(DATA_DIR / "complex.maxpat").to_dict()
38
68
  with open(DATA_DIR / "complex.maxpat") as f:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes