py2max 0.3.6__tar.gz → 0.4.0__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 (191) hide show
  1. {py2max-0.3.6 → py2max-0.4.0}/CHANGELOG.md +366 -0
  2. {py2max-0.3.6 → py2max-0.4.0}/PKG-INFO +54 -8
  3. {py2max-0.3.6 → py2max-0.4.0}/README.md +53 -7
  4. {py2max-0.3.6 → py2max-0.4.0}/py2max/__init__.py +18 -2
  5. {py2max-0.3.6 → py2max-0.4.0}/py2max/cli.py +17 -0
  6. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/abstract.py +1 -0
  7. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/box.py +74 -8
  8. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/factory.py +182 -48
  9. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/patcher.py +20 -4
  10. py2max-0.4.0/py2max/core/props.py +1748 -0
  11. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/serialization.py +25 -2
  12. py2max-0.4.0/py2max/data/js2max/js2max.js +2017 -0
  13. py2max-0.4.0/py2max/data/js2max/js2max.v8.js +2620 -0
  14. py2max-0.4.0/py2max/js2max_runtime.py +123 -0
  15. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/matrix.py +10 -2
  16. {py2max-0.3.6 → py2max-0.4.0}/py2max/log.py +124 -52
  17. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/db.py +44 -13
  18. {py2max-0.3.6 → py2max-0.4.0}/pyproject.toml +1 -1
  19. {py2max-0.3.6 → py2max-0.4.0}/tests/conftest.py +25 -2
  20. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/README.md +10 -1
  21. py2max-0.4.0/tests/test_add.py +176 -0
  22. py2max-0.4.0/tests/test_box_props.py +186 -0
  23. {py2max-0.3.6 → py2max-0.4.0}/tests/test_comment.py +15 -0
  24. {py2max-0.3.6 → py2max-0.4.0}/tests/test_db.py +76 -0
  25. py2max-0.4.0/tests/test_js2max_runtime.py +258 -0
  26. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_matrix.py +62 -0
  27. py2max-0.4.0/tests/test_logging.py +189 -0
  28. py2max-0.4.0/tests/test_param.py +107 -0
  29. py2max-0.4.0/tests/test_serialization.py +130 -0
  30. py2max-0.4.0/tests/test_single_file.py +369 -0
  31. py2max-0.4.0/tests/test_subpatch.py +98 -0
  32. py2max-0.3.6/tests/test_add.py +0 -65
  33. py2max-0.3.6/tests/test_param.py +0 -36
  34. py2max-0.3.6/tests/test_subpatch.py +0 -34
  35. {py2max-0.3.6 → py2max-0.4.0}/LICENSE +0 -0
  36. {py2max-0.3.6 → py2max-0.4.0}/py2max/__main__.py +0 -0
  37. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/__init__.py +0 -0
  38. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/colors.py +0 -0
  39. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/common.py +0 -0
  40. {py2max-0.3.6 → py2max-0.4.0}/py2max/core/patchline.py +0 -0
  41. {py2max-0.3.6 → py2max-0.4.0}/py2max/exceptions.py +0 -0
  42. {py2max-0.3.6 → py2max-0.4.0}/py2max/export/__init__.py +0 -0
  43. {py2max-0.3.6 → py2max-0.4.0}/py2max/export/converters.py +0 -0
  44. {py2max-0.3.6 → py2max-0.4.0}/py2max/export/svg.py +0 -0
  45. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/__init__.py +0 -0
  46. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/base.py +0 -0
  47. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/external.py +0 -0
  48. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/flow.py +0 -0
  49. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/graph.py +0 -0
  50. {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/grid.py +0 -0
  51. {py2max-0.3.6 → py2max-0.4.0}/py2max/lint.py +0 -0
  52. {py2max-0.3.6 → py2max-0.4.0}/py2max/m4l.py +0 -0
  53. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/__init__.py +0 -0
  54. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/category.py +0 -0
  55. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/data/bundle.json.gz +0 -0
  56. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/legacy.py +0 -0
  57. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/parser.py +0 -0
  58. {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/porttypes.py +0 -0
  59. {py2max-0.3.6 → py2max-0.4.0}/py2max/py.typed +0 -0
  60. {py2max-0.3.6 → py2max-0.4.0}/py2max/transformers.py +0 -0
  61. {py2max-0.3.6 → py2max-0.4.0}/py2max/utils.py +0 -0
  62. {py2max-0.3.6 → py2max-0.4.0}/tests/__init__.py +0 -0
  63. {py2max-0.3.6 → py2max-0.4.0}/tests/data/complex.maxpat +0 -0
  64. {py2max-0.3.6 → py2max-0.4.0}/tests/data/desc.maxpat +0 -0
  65. {py2max-0.3.6 → py2max-0.4.0}/tests/data/empty.maxpat +0 -0
  66. {py2max-0.3.6 → py2max-0.4.0}/tests/data/mydevice.amxd +0 -0
  67. {py2max-0.3.6 → py2max-0.4.0}/tests/data/mydevice2.amxd +0 -0
  68. {py2max-0.3.6 → py2max-0.4.0}/tests/data/nested.maxpat +0 -0
  69. {py2max-0.3.6 → py2max-0.4.0}/tests/data/simple.maxpat +0 -0
  70. {py2max-0.3.6 → py2max-0.4.0}/tests/data/tabular.maxpat +0 -0
  71. {py2max-0.3.6 → py2max-0.4.0}/tests/data/umenu.maxref.xml +0 -0
  72. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/connection_patterns.py +0 -0
  73. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/custom_extensions.py +0 -0
  74. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/data_containers.py +0 -0
  75. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/error_handling.py +0 -0
  76. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/performance_optimization.py +0 -0
  77. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/subpatchers.py +0 -0
  78. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/api/box_api_examples.py +0 -0
  79. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/api/patcher_api_examples.py +0 -0
  80. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/auto_layout_demo.py +0 -0
  81. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/db/category_db_demo.py +0 -0
  82. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/db/maxref_db_demo.py +0 -0
  83. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/columnar_layout_examples.py +0 -0
  84. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/flow_layout_examples.py +0 -0
  85. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/grid_layout_examples.py +0 -0
  86. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/matrix_layout_examples.py +0 -0
  87. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/basic_synth.maxpat +0 -0
  88. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/basic_synth.svg +0 -0
  89. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/complex_synth.maxpat +0 -0
  90. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/complex_synth.svg +0 -0
  91. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/flow_layout.maxpat +0 -0
  92. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/flow_layout.svg +0 -0
  93. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/grid_layout.maxpat +0 -0
  94. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/grid_layout.svg +0 -0
  95. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  96. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/horizontal_layout.svg +0 -0
  97. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_no_ports.svg +0 -0
  98. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_no_title.svg +0 -0
  99. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_patch.maxpat +0 -0
  100. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_with_ports.svg +0 -0
  101. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/svg_preview_demo.py +0 -0
  102. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/vertical_layout.maxpat +0 -0
  103. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/vertical_layout.svg +0 -0
  104. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/workflow_demo.maxpat +0 -0
  105. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/workflow_demo.svg +0 -0
  106. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/quickstart/basic_patch.py +0 -0
  107. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/quickstart/layout_examples.py +0 -0
  108. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/generative_music.py +0 -0
  109. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/interactive_controller.py +0 -0
  110. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  111. {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/simple_synthesis.py +0 -0
  112. {py2max-0.3.6 → py2max-0.4.0}/tests/graphs/random/v30e33.tglf +0 -0
  113. {py2max-0.3.6 → py2max-0.4.0}/tests/test_abstract_coverage.py +0 -0
  114. {py2max-0.3.6 → py2max-0.4.0}/tests/test_abstraction.py +0 -0
  115. {py2max-0.3.6 → py2max-0.4.0}/tests/test_amxd.py +0 -0
  116. {py2max-0.3.6 → py2max-0.4.0}/tests/test_attrui.py +0 -0
  117. {py2max-0.3.6 → py2max-0.4.0}/tests/test_basic.py +0 -0
  118. {py2max-0.3.6 → py2max-0.4.0}/tests/test_beap.py +0 -0
  119. {py2max-0.3.6 → py2max-0.4.0}/tests/test_bpatcher.py +0 -0
  120. {py2max-0.3.6 → py2max-0.4.0}/tests/test_cli.py +0 -0
  121. {py2max-0.3.6 → py2max-0.4.0}/tests/test_coll.py +0 -0
  122. {py2max-0.3.6 → py2max-0.4.0}/tests/test_colors.py +0 -0
  123. {py2max-0.3.6 → py2max-0.4.0}/tests/test_colors_theme.py +0 -0
  124. {py2max-0.3.6 → py2max-0.4.0}/tests/test_connection_validation.py +0 -0
  125. {py2max-0.3.6 → py2max-0.4.0}/tests/test_converters.py +0 -0
  126. {py2max-0.3.6 → py2max-0.4.0}/tests/test_core_coverage.py +0 -0
  127. {py2max-0.3.6 → py2max-0.4.0}/tests/test_defaults.py +0 -0
  128. {py2max-0.3.6 → py2max-0.4.0}/tests/test_dict.py +0 -0
  129. {py2max-0.3.6 → py2max-0.4.0}/tests/test_edit_operations.py +0 -0
  130. {py2max-0.3.6 → py2max-0.4.0}/tests/test_encapsulate.py +0 -0
  131. {py2max-0.3.6 → py2max-0.4.0}/tests/test_error_handling.py +0 -0
  132. {py2max-0.3.6 → py2max-0.4.0}/tests/test_examples.py +0 -0
  133. {py2max-0.3.6 → py2max-0.4.0}/tests/test_ezdac.py +0 -0
  134. {py2max-0.3.6 → py2max-0.4.0}/tests/test_gen.py +0 -0
  135. {py2max-0.3.6 → py2max-0.4.0}/tests/test_group.py +0 -0
  136. {py2max-0.3.6 → py2max-0.4.0}/tests/test_itable.py +0 -0
  137. {py2max-0.3.6 → py2max-0.4.0}/tests/test_js.py +0 -0
  138. {py2max-0.3.6 → py2max-0.4.0}/tests/test_kwds_filter.py +0 -0
  139. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout.py +0 -0
  140. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_box_dims.py +0 -0
  141. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_builtins.py +0 -0
  142. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_coverage.py +0 -0
  143. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_flow.py +0 -0
  144. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_graph.py +0 -0
  145. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_graph_layout.py +0 -0
  146. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_graph_manager.py +0 -0
  147. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_overlaps.py +0 -0
  148. {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_vertical.py +0 -0
  149. {py2max-0.3.6 → py2max-0.4.0}/tests/test_linking.py +0 -0
  150. {py2max-0.3.6 → py2max-0.4.0}/tests/test_lint.py +0 -0
  151. {py2max-0.3.6 → py2max-0.4.0}/tests/test_m4l.py +0 -0
  152. {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref.py +0 -0
  153. {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_bundle.py +0 -0
  154. {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_lazy.py +0 -0
  155. {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_parser.py +0 -0
  156. {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_refpages.py +0 -0
  157. {py2max-0.3.6 → py2max-0.4.0}/tests/test_mc_cycle.py +0 -0
  158. {py2max-0.3.6 → py2max-0.4.0}/tests/test_mc_poly.py +0 -0
  159. {py2max-0.3.6 → py2max-0.4.0}/tests/test_message.py +0 -0
  160. {py2max-0.3.6 → py2max-0.4.0}/tests/test_mypatch.py +0 -0
  161. {py2max-0.3.6 → py2max-0.4.0}/tests/test_nested.py +0 -0
  162. {py2max-0.3.6 → py2max-0.4.0}/tests/test_nested_patchers.py +0 -0
  163. {py2max-0.3.6 → py2max-0.4.0}/tests/test_number_tilde.py +0 -0
  164. {py2max-0.3.6 → py2max-0.4.0}/tests/test_numbers.py +0 -0
  165. {py2max-0.3.6 → py2max-0.4.0}/tests/test_param_placement.py +0 -0
  166. {py2max-0.3.6 → py2max-0.4.0}/tests/test_patcher.py +0 -0
  167. {py2max-0.3.6 → py2max-0.4.0}/tests/test_pitched_osc.py +0 -0
  168. {py2max-0.3.6 → py2max-0.4.0}/tests/test_porttypes.py +0 -0
  169. {py2max-0.3.6 → py2max-0.4.0}/tests/test_presets.py +0 -0
  170. {py2max-0.3.6 → py2max-0.4.0}/tests/test_pydantic.py +0 -0
  171. {py2max-0.3.6 → py2max-0.4.0}/tests/test_rnbo.py +0 -0
  172. {py2max-0.3.6 → py2max-0.4.0}/tests/test_rnbo_subpatcher.py +0 -0
  173. {py2max-0.3.6 → py2max-0.4.0}/tests/test_scripting_name.py +0 -0
  174. {py2max-0.3.6 → py2max-0.4.0}/tests/test_search.py +0 -0
  175. {py2max-0.3.6 → py2max-0.4.0}/tests/test_semantic_ids.py +0 -0
  176. {py2max-0.3.6 → py2max-0.4.0}/tests/test_svg.py +0 -0
  177. {py2max-0.3.6 → py2max-0.4.0}/tests/test_svg_fidelity.py +0 -0
  178. {py2max-0.3.6 → py2max-0.4.0}/tests/test_table.py +0 -0
  179. {py2max-0.3.6 → py2max-0.4.0}/tests/test_transformers.py +0 -0
  180. {py2max-0.3.6 → py2max-0.4.0}/tests/test_transformers_e4.py +0 -0
  181. {py2max-0.3.6 → py2max-0.4.0}/tests/test_tree.py +0 -0
  182. {py2max-0.3.6 → py2max-0.4.0}/tests/test_tree_builder.py +0 -0
  183. {py2max-0.3.6 → py2max-0.4.0}/tests/test_tutorial_simple_synthesis.py +0 -0
  184. {py2max-0.3.6 → py2max-0.4.0}/tests/test_two_sines.py +0 -0
  185. {py2max-0.3.6 → py2max-0.4.0}/tests/test_umenu.py +0 -0
  186. {py2max-0.3.6 → py2max-0.4.0}/tests/test_utils.py +0 -0
  187. {py2max-0.3.6 → py2max-0.4.0}/tests/test_validate_attrs.py +0 -0
  188. {py2max-0.3.6 → py2max-0.4.0}/tests/test_validation.py +0 -0
  189. {py2max-0.3.6 → py2max-0.4.0}/tests/test_varname.py +0 -0
  190. {py2max-0.3.6 → py2max-0.4.0}/tests/test_wheel_bundle.py +0 -0
  191. {py2max-0.3.6 → py2max-0.4.0}/tests/test_zl_group.py +0 -0
@@ -2,12 +2,199 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.0] - 2026-07-27
6
+
7
+ Two things dominate this release.
8
+
9
+ **js2max**: a JavaScript counterpart to py2max that runs *inside* an open Max
10
+ patcher through the `v8` object, so a patch can build objects into itself and
11
+ serialize itself back out. Its runtime ships in the wheel, so
12
+ `p.add_v8_bridge()` and `p.save()` are all it takes. Confirmed end to end
13
+ against Max, including a file js2max wrote being opened by Max.
14
+
15
+ **Typed box properties**: the Max property vocabulary reaches the emitted patch
16
+ through `**kwds` no longer. `BoxProps` / `TextboxProps` mean a misspelled
17
+ `bgcolour` or a wrongly-typed `fontsize` is rejected by mypy with a suggestion,
18
+ where both previously shipped straight into the file.
19
+
20
+ The rest is nine fixes, several of them long-standing and silent -- comments
21
+ written with their port counts backwards since the beginning, `to_dict()`
22
+ returning an empty patcher depending on call order, and nulls reaching the patch
23
+ one level down.
24
+
25
+ ### New: js2max -- a JavaScript counterpart that builds patches inside Max
26
+
27
+ - **[`js2max/`](js2max/) is a second front end to the `.maxpat` format**, in TypeScript, doing the one thing this package cannot: Max embeds a JavaScript engine, and `v8` ships in current Max, so a script runs *inside an open patcher* and builds into it directly. py2max writes files Max later opens; js2max builds into the patch that is already open, from the same description. It serializes the other way too, and a file it wrote has been opened in Max.
28
+
29
+ - **Nothing changes for Python users.** js2max adds no dependency -- its bundles are inert data, and nothing in py2max imports or executes them; the package keeps its zero runtime dependencies. It is a sibling directory with its own toolchain ([Bun](https://bun.sh)) and its own [CHANGELOG](https://github.com/shakfu/py2max/blob/main/js2max/CHANGELOG.md), where the detail lives. The two loadable bundles are committed, so a Max user needs no toolchain either.
30
+
31
+ - **The two packages share one description of the format, not two.** `js2max/src/objects.ts` is generated from py2max's maxref bundle by `scripts/gen_js2max_objects.py`, so the box class, port counts and outlet types of all 1098 known object classes come from one source; the Max version a written file declares is exported the same way, rather than restated. The verification patches under `js2max/max/` are generated by py2max itself (`scripts/gen_v8_harness.py`) -- the Python package emits a patch that loads the JavaScript bundle that builds objects from the format the Python package writes.
32
+
33
+ - `make js2max` builds it, `make js2max-check` typechecks it, runs its 236 tests, and fails if any generated file or committed bundle has drifted from its source. CI runs the latter.
34
+
35
+ - One py2max bug came back the other way: `add_comment` had its port counts backwards, found by serializing a py2max patch out of Max and diffing it against the original. See the `add_comment` entry in this release.
36
+
37
+ ### New: the js2max runtime ships in the wheel; `add_v8_bridge()` writes it beside a patch
38
+
39
+ - The built JavaScript now lives at `py2max/data/js2max/` and ships as package data, so **`pip install py2max` is enough to use js2max**. Until now the bundles existed only in the source repository, which meant the feature was unreachable for everyone who installs from PyPI.
40
+
41
+ - `p.add_v8_bridge()` adds a `[v8 js2max.v8.js]` box and marks the patcher; `save()` then writes the runtime next to the patch, because Max resolves a bare filename through the folder holding it. A patcher that never asked for the bridge writes only itself, so an ordinary `save()` cannot leave a stray `.js` file behind, and `add_v8_bridge(bundle="my.js")` installs nothing -- naming your own file means you placed it. `py2max.js2max_runtime.path()` / `install()` are the direct API.
42
+
43
+ - **Shipping them together is a correctness guarantee, not a convenience.** `js2max/src/objects.ts` -- box classes, port counts and outlet types for 1098 object classes -- is generated from py2max's maxref bundle. A runtime built against one version of py2max and paired with another declares wrong port counts for whatever changed, and a box declaring a port it does not have silently loses the cord attached to it when Max opens the file. One artifact makes that skew impossible, and `tests/test_js2max_runtime.py` asserts the two agree rather than leaving it as an intention.
44
+
45
+ - No new dependency: the bundles are inert data, like the maxref bundle, and nothing in py2max imports or executes them. They add 160 KB to a wheel whose maxref data is already 1.0 MB. The build writes both copies (`js2max/max/` for Max, `py2max/data/js2max/` for the wheel) and `make js2max-check` fails if they differ, so the shipped runtime cannot lag the repository one.
46
+
47
+ - The single-file edition (`scripts/py2max.py`) cannot carry 160 KB of bundled JavaScript, so `js2max_runtime` is stubbed there as `layout="graph:*"` already is: `add_v8_bridge()` still builds the box, and saving raises `NotImplementedError` naming the full package -- unless you pass `bundle=` for a runtime you placed yourself, which works in both editions.
48
+
49
+ - Documented in [the js2max guide](https://github.com/shakfu/py2max/blob/main/docs/user_guide/js2max.md).
50
+
51
+ ### New: box properties are typed -- a misspelled property is now an error, not a silent key
52
+
53
+ - Max box properties reached the emitted `.maxpat` through `**kwds: Any`, so `p.add_textbox("cycle~ 440", bgcolour=[0,0,0,1])` wrote `bgcolour` into the patch and `fontsize="twelve"` wrote a string where Max wants a number. Neither was caught by anything: `mypy --strict` passes on `Any`, and `validate_attrs=True` warns about unknown property *names* at runtime but says nothing about *types* and emits the key regardless.
54
+
55
+ - **`py2max/core/props.py` (generated) defines `BoxProps` / `TextboxProps`**, and `Box.__init__`, `add_textbox`, `add_message` and `add_comment` now accept `**kwds: Unpack[BoxProps]`. Under the `mypy --strict` this project already runs, a misspelled property is rejected *with a suggestion* (`did you mean "bgcolor", "bgcolor2", or "hbgcolor"?`), as are a wrongly-typed value and an explicit `None` for an optional property.
56
+
57
+ - Zero runtime cost and no new dependency: `Unpack` is imported under `if TYPE_CHECKING` with postponed annotations, so nothing is evaluated at runtime and the library keeps shipping zero runtime dependencies (verified by running the single-file build on a Python with no `typing_extensions` installed). `BoxProps` and `TextboxProps` are exported from the top level for callers annotating their own helpers.
58
+
59
+ - **Two TypedDicts rather than one, deliberately.** A TypedDict key that collides with a named parameter makes mypy report `Overlap between argument names and ** TypedDict items` **and then stop checking calls to that function altogether** -- a silent loss of coverage, which is the worst possible failure for a change whose entire purpose is coverage. `BoxProps` therefore omits `Box.__init__`'s five structural parameters and `TextboxProps` additionally omits `text`/`outlettype`/`comment`/`comment_pos`/`justify`; the narrower flows into the wider when kwds are forwarded. `tests/test_box_props.py::test_the_overlap_trap_is_absent` guards against a regression.
60
+
61
+ - **The 850-property vocabulary is generated from four sources** by `scripts/gen_box_props.py` (`make box-props`), but they are nowhere near equal partners. maxref attributes whose `save` meta-attribute is 1 -- exactly those Max persists into a file -- supply 830 names, 785 of them found nowhere else. A hand-written table adds 20 more and, more usefully, **retypes 7** that maxref declares too widely: the generator unions an attribute's type across every object declaring it, which is correct where the key genuinely differs but costs real checking on the handful users actually pass (`range` arrives as `Union[Sequence[float], Sequence[int], float, int]` and is pinned to `Sequence[Atom]`). Three keys are dropped for not being valid Python identifiers (`one/column`, `one/matrix`, `one/row`).
62
+
63
+ - **The other two sources contribute one name each -- and that is why they exist.** An AST scan of every keyword py2max itself passes to `Box(...)` adds `viewvisibility`: maxref documents `bpatcher` with 12 attributes and omits this one, which the library writes and Max accepts, so a maxref-only vocabulary would have rejected py2max's own output. A sweep of the repository's `.maxpat` fixtures adds `comment`. Both scans are cheap, and they are the only defence against maxref being incomplete, which it demonstrably is.
64
+
65
+ - The result is typed rather than nominally typed: of the 850 properties only five are bare `Any` (`comment`, `data`, `outlettype`, `patcher`, `viewvisibility`); the rest resolve to `int` (481), `Sequence[float]` (154), `str` (92), `float` (68) and small unions.
66
+
67
+ - **Known limitation:** the vocabulary is a flat union across all 1175 objects, and the median maxref attribute is declared by exactly one of them (the most widely shared, `bgcolor`, by 76). So `BoxProps` cannot tell that a property is invalid *for the maxclass it was passed to*: 547 of the 850 belong to a single object, and `p.add_textbox("cycle~ 440", activedialcolor=[1.0, 0.0, 0.0, 1.0])` type-checks cleanly and writes `activedialcolor` into the patch even though only `live.dial` declares it. Misspelled and wrongly-typed properties are rejected; real properties on the wrong object are not. Recorded in `TODO.md`.
68
+
69
+ - `add_floatparam` / `add_intparam` now pass `minimum`/`maximum` through `kwds_filter` instead of forwarding `None`, so an unset bound is absent from the patch rather than present as null.
70
+
71
+ - `tests/test_box_props.py` (13 tests) runs mypy in subprocesses to assert each failure mode is rejected and that correct usage still checks -- a static guarantee is not observable at runtime, so an ordinary assertion cannot see it. A staleness guard fails if the generated file drifts from its sources.
72
+
73
+ - Those subprocesses pass `--no-color-output`. mypy honours a `FORCE_COLOR` inherited from the developer's shell, and the assertions match on message text, so without it the tests failed for anyone who has it set -- and `test_the_overlap_trap_is_absent`, which asserts a fragment is *absent*, would instead have passed for the wrong reason.
74
+
75
+ ### New: `scripts/py2max.py` is now generated, not hand-maintained
76
+
77
+ - `scripts/py2max.py` -- the single-file edition -- is now produced by `scripts/build_single_file.py` (`make single-file`) instead of being maintained by hand. It amalgamates the core object model, the grid/flow/columnar/matrix layout managers, `lint()`, connection and attribute validation, `.amxd` read/write and SVG export into one module, with an offline maxref table embedded (port types, method names and attribute names for all 1175 objects, compressed to ~30 KB; the ~8 MB of documentation prose is dropped, so `Box.help()` returns a pointer to the full package while `get_info()` still returns structured data). Graph layouts (`layout="graph:*"`), the CLI and the SQLite maxref database are excluded; `layout="graph:*"` raises `NotImplementedError` naming the package to install.
78
+
79
+ - **Why:** the hand-maintained version had drifted five releases behind and carried four real defects, including one that broke *every* edit-after-load (`width` read `self.rect.w`, but a loaded patch keeps `rect` as a plain JSON list) and the operator-precedence bug in `add_coll`/`add_dict`/`add_table` that discarded a caller-supplied `text`. Nothing in the repo imported it, so no test caught any of it. Generating the file makes drift structurally impossible: the single file now *is* the package's own code.
80
+
81
+ - `tests/test_single_file.py` asserts equivalence rather than mere importability: 13 patch builders (layouts, containers, subpatchers, semantic ids, editing, theming) are built with both implementations and their emitted JSON must match exactly, plus per-object agreement on port counts, validation verdicts and messages, `MAXCLASS_DEFAULTS`, object coverage, lint findings and SVG output. A staleness guard runs the generator with `--check` and fails if the committed file differs from a fresh build, so a stale copy can no longer rot unnoticed.
82
+
83
+ - The generator refuses to emit a broken file: it fails on top-level name collisions between amalgamated modules, and on any undefined name left behind when included code calls into an excluded module (which is how the missing SVG exporter was caught). Builds are byte-reproducible (the gzip header's timestamp is zeroed), so the staleness check is meaningful.
84
+
85
+ ### Fixed: `add_comment` had its port counts backwards
86
+
87
+ - A comment box takes a `set` message and emits nothing -- 1 inlet, 0 outlets. `add_comment` passed neither to `Box`, so the constructor's defaults applied and produced 0 inlets and 1 outlet on every comment py2max has ever written.
88
+
89
+ - **Found by round-tripping a py2max patch through Max**: js2max serialized a patcher back to a `.maxpat`, and diffing it against the py2max original showed Max had rewritten the values on save. Max is the authority, and it disagreed.
90
+
91
+ - Related, in the generated js2max object table: `Box.__init__` defaults `numoutlets` to 1, and for the **73 objects maxref does not state it** that default is simply wrong -- `print` came out with an outlet it does not have. The table now reads maxref directly and omits a count maxref is silent about, since Max derives ports from the instantiated object anyway.
92
+
93
+ ### Fixed: `to_dict()` returned an empty patcher until something else rendered
94
+
95
+ - `p.to_dict()["patcher"]["boxes"]` was empty on a patcher full of boxes, and stayed empty until `to_json()` or `save()` happened to call `render()` -- after which the *same call on the same object* started returning them. Order-dependent, and silent: a test asserting over `to_dict()` examined an empty patcher and passed for the wrong reason, which is how it was found.
96
+
97
+ - **`to_dict()` now renders on its own behalf.** Renaming it was the alternative, and would have preserved the trap under a new name across 104 call sites; rendering also removes the asymmetry with `Box.to_dict()`, which has always returned a populated box with no preparation required. `to_json()` no longer renders separately, since `to_dict()` does it.
98
+
99
+ - **`render()` is now idempotent**, which the above requires -- `save_as()` renders for its own log line and `to_dict()` renders again underneath it. `self.boxes` was *appended* to while `self.lines` was rebuilt, so a second render duplicated every box and no line. That path is reachable only through `reset_on_render=False`, which nothing in the repository passes, so the asymmetry had never bitten; it made rendering order-dependent in exactly the way `to_dict()` was.
100
+
101
+ - `tests/test_serialization.py` (11 tests) pins both properties: that `to_dict()` is self-sufficient and repeatable, and that rendering, saving or serializing more than once cannot duplicate a box -- including for subpatchers, whose contents render one level down.
102
+
103
+ ### Fixed: box port counts -- explicit zeros, and subpatchers that track their contents
104
+
105
+ - `Box.__init__` used `numoutlets or 1` / `numinlets or 0`, so an explicit `numoutlets=0` was silently promoted to 1: an object deliberately created with no outlets still claimed one, and could therefore be used as a connection source. Both now use `x if x is not None else default`, so a meaningful 0 survives. (The `numinlets` line was not actually defective -- its default is already 0 -- but is spelled the same way for clarity.)
106
+
107
+ - **A subpatcher box now declares as many ports as it really has.** `inlet` / `outlet` objects are normally added to a nested patcher *after* the subpatcher box exists, so the counts fixed at construction went stale, and Max renders a box's declared count -- a `p sub` holding three `outlet` objects but declaring one emitted a patch whose other two outlets could not be connected. `Box.render()` now syncs the counts (and `outlettype`) from the nested patcher's `inlet`/`outlet` objects, reusing the same derivation `lint()` already used to report the discrepancy.
108
+
109
+ - The sync only overrides a dimension it actually counted objects for, because a nested patcher can hold I/O that this cannot interpret: `gen~` and `rnbo~` declare theirs with `in`/`out` objects, and an empty subpatcher is a stub the caller has yet to fill. Zeroing those boxes' ports would be worse than keeping the constructed default, so they are left alone.
110
+
111
+ - `add_subpatcher` now states its port defaults (1 inlet, 1 outlet) explicitly instead of passing a falsy `0` and relying on `Box.__init__` to promote it -- which is what the previous `numoutlets or 0` did in practice. This keeps `gen~`/`rnbo~` boxes, which are created through this path, exactly as before.
112
+
113
+ - Regression tests in `tests/test_subpatch.py` cover all four cases: explicit zeros, port tracking, empty-subpatcher defaults, and `gen~`/`rnbo~` ports surviving.
114
+
115
+ ### Fixed: nulls no longer reach the patch -- `_remove_none_entries` recurses
116
+
117
+ - `Box._remove_none_entries` dropped None-valued keys only at the top level, so anything one level down survived. `add_intparam` writes `parameter_mmax` unconditionally, which meant an unset maximum shipped as `"parameter_mmax": null` inside `saved_attribute_attributes` -- and Max distinguishes an absent key from a null one. (`add_floatparam` omits the key entirely; the asymmetry was the tell, and the method's own `TODO: make recursive` was the same symptom.) Fixed by making the scrub recursive rather than special-casing the key, which closes the class instead of the instance.
118
+
119
+ - Lists are walked but **not** filtered: a None *element* is positional -- an `outlettype` slot, say -- so dropping it would change the arity. Tuples are left alone so `Rect`, a NamedTuple, survives as itself. Loading is unaffected, since `Box.from_dict` bypasses `__init__` entirely.
120
+
121
+ - `tests/test_param.py` now asserts that a representative patch contains no nulls at any depth. It reads back `to_json()` rather than `to_dict()`, because only the former renders the boxes -- asserting over `to_dict()` inspects an empty patcher and passes for the wrong reason. That trap is recorded in `TODO.md`.
122
+
123
+ ### Fixed: `add()` raised `TypeError` when a keyword named the target's own parameter
124
+
125
+ - `Patcher.add()` derives the first argument of its target method from the value it was handed (the text tail, the number) and then forwards `**kwds` to the same call, so a caller naming that parameter passed it twice: `p.add("cycle~ 440", text="saw~ 220")`, `p.add(5, initial=3)` and `p.add("coll x", name="y")` all failed with `got multiple values for argument`. An explicit keyword now wins over the derived value.
126
+
127
+ - **This was a family, not a list of cases.** The `_maxclass_methods` branch fills *every* specialized method's first parameter positionally, so `add_coll(name=)`, `add_dict(name=)`, `add_table(name=)`, `add_itable(name=)`, `add_umenu(prefix=)`, `add_bpatcher(name=)`, `add_message(text=)` and `add_comment(text=)` collided as well, alongside `add_textbox(text=)`, `add_subpatcher(text=)`, `add_gen_codebox(code=)`, `add_rnbo(text=)` and `add_floatparam`/`add_intparam`'s `initial=`. A single `_dispatch` helper now applies one rule at every branch, and finds the parameter name by introspection rather than from a table, so adding an entry to `Patcher._maxclass_methods` cannot silently reintroduce the collision. The test drives off that table for the same reason.
128
+
129
+ - **`.add(<number>, name=...)` no longer leaks a stray `name` property into the patch.** The keyword names the *parameter* (it becomes `parameter_longname`), but it was read without being removed from `**kwds`, so it was also emitted as a box property. The positional form, `.add(1.5, "freq")`, is unchanged and still wins over a `name=` keyword.
130
+
131
+ - **Fixed alongside: `add_umenu()` crashed unless `items` was given** -- `TypeError: object of type 'NoneType' has no len()` -- despite the parameter being optional, which also meant `p.add("umenu")` had never worked. The `cast(List[str], items)` masking the `Optional` from mypy was the tell.
132
+
133
+ ### Fixed: `MaxRefDB.search()` treated `%` and `_` as wildcards
134
+
135
+ - The query was interpolated into a `LIKE` pattern unescaped, so SQL metacharacters in a search term were executed rather than matched. `search("%")` returned the entire database (1175 objects), `search("_")` likewise, and `search("gain_")` returned 16 unrelated objects because `_` matches any single character. Since Max object names routinely contain `_` (`jit_kernel`), this was reachable in ordinary use. The term is now escaped and the clause declares `ESCAPE '\'`.
136
+
137
+ - **`search()` with no recognized field now raises `ValueError`** instead of building `WHERE ORDER BY` and failing inside sqlite with `OperationalError: near "ORDER": syntax error`. The recognized set is exposed as `MaxRefDB.SEARCHABLE_FIELDS`.
138
+
139
+ ### Fixed: matrix layout fragmented a signal chain when boxes were added in reverse order
140
+
141
+ - `MatrixLayoutManager` treated any object with *at most one* input as the start of a signal chain, which makes every mid-chain object a chain start. A chain stops as soon as it reaches an object another chain already claimed, so whichever mid-chain object came first in iteration order consumed the tail and stranded the real source: `cycle~ -> gain~ -> ezdac~` was detected as **three** chains -- and therefore laid out as three matrix columns -- purely because the boxes were added in reverse signal order. A chain start is now an object with no inputs at all.
142
+
143
+ - The recorded symptom for this was "fix cycle handling", but cycles were never the defect: pure cycles, cycles with an external feeder, and self-loops all traced correctly before and after. Regression tests in `tests/test_layout_matrix.py` cover creation order, parallel chains meeting at a shared sink, all three cycle shapes, and disconnected objects.
144
+
145
+ ### Fixed: importing py2max no longer logs, nor hijacks the host's logging
146
+
147
+ - **`import py2max` is now silent.** DEBUG-level logging was previously the shipped default (`log.py`: `DEBUG = getenv("DEBUG", default=True)`), so simply constructing a `Patcher` printed internal diagnostics to the console. Logging is now opt-in.
148
+
149
+ - **Breaking (bad default removed): the library no longer calls `logging.basicConfig(..., force=True)` at import.** That call replaced the host application's logging configuration -- handlers, format and level -- as a side effect of importing py2max. A program that configured `basicConfig(format="APP: %(message)s")` and then imported py2max silently lost its own format. The `py2max` logger now carries a `NullHandler` and nothing else is touched, which is the standard way for a library to participate in logging without imposing any.
150
+
151
+ - **New `py2max.setup_logging(level=..., color=..., log_file=...)`** opts in to py2max's colored console output. It attaches handlers to the `py2max` logger only, never to root, and is idempotent (repeat calls replace their own handlers rather than stacking duplicates). It deliberately leaves `propagate` alone: disabling it would be a global side effect that silently blinds anything capturing py2max records through an ancestor logger, including pytest's `caplog`.
152
+
153
+ - **Env vars are namespaced and default off:** `PY2MAX_DEBUG=1` (was the bare, extremely common `DEBUG`, which meant any unrelated `DEBUG=1` in the environment turned py2max verbose), plus `PY2MAX_LOG_LEVEL`, `PY2MAX_LOG_FILE` and `PY2MAX_COLOR`. Setting any of the first three enables logging at import; an explicit `PY2MAX_DEBUG=0` stays silent.
154
+
155
+ - **The CLI, being an application rather than a library, now configures logging explicitly** and gained `-v`/`-vv` (INFO/DEBUG) and `-q` flags. It previously got its output purely as a side effect of importing the package.
156
+
157
+ - `config()` is retained as a no-op alias of `get_logger()` for backwards compatibility.
158
+
159
+ - `tests/test_logging.py` covers all of it, using subprocesses for the import-time behaviour that cannot be re-tested in an already-imported module. A `tests/conftest.py` fixture now snapshots and restores the `py2max` logger around every test: `setup_logging()` mutates process-global state, and without isolation a CLI test's configuration made `caplog` blind in a later lint test -- a failure that passed in isolation and only appeared in the full run.
160
+
161
+ ### Fixed: the maxref cache no longer prints to stderr
162
+
163
+ - Building the one-time object cache announced itself with four `print(..., file=sys.stderr)` calls. A library does not get to decide whether that is visible or where it goes; it is now logged at INFO on the `py2max` logger, so `py2max.setup_logging("INFO")` shows it and the default stays silent. The CLI still prints -- it is an application, and its output is the product.
164
+
165
+ ### Notes for upgraders
166
+
167
+ No API was removed and no call signature changed, but four things behave
168
+ differently enough to mention.
169
+
170
+ - **Every comment box changes shape.** `add_comment` wrote 0 inlets and 1 outlet;
171
+ a comment has 1 and 0. Regenerating a patch that contains comments produces a
172
+ different -- correct -- file, and Max was silently rewriting the values on save
173
+ anyway.
174
+ - **`to_dict()` renders**, so it returns the patcher as it stands rather than as
175
+ it stood after whatever last rendered it. It previously returned an empty
176
+ patcher until something else called `render()`. If you were calling
177
+ `render()` first, you no longer need to; if you were relying on the empty
178
+ result, you were relying on a bug.
179
+ - **`render()` is idempotent.** `reset_on_render=False` no longer accumulates
180
+ boxes across renders. It also never accumulated *lines*, so what it did before
181
+ was not coherent.
182
+ - **`save()` may now write a second file** -- but only for a patcher that called
183
+ `add_v8_bridge()`, which is new in this release. Nothing that worked before
184
+ writes anything extra.
185
+
186
+ Typed box properties are a static change: `mypy --strict` will now reject a
187
+ misspelled or wrongly-typed property that it previously accepted. That is the
188
+ point of them, and nothing changes at runtime.
189
+
5
190
  ## [0.3.6]
6
191
 
7
192
  ### Removed: incremental layout; `optimize_layout()` is batch-only again
8
193
 
9
194
  - `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).
195
+
10
196
  - **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.
197
+
11
198
  - **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
199
 
13
200
  ## [0.3.5]
@@ -25,12 +212,19 @@
25
212
  ### New: Patch linting and message-type-aware connection validation
26
213
 
27
214
  - Added `Patcher.lint()` (and the `py2max.lint` module: `lint()`, `Finding`) -- a patch-level health check returning structured findings with a `severity`, a `code`, and object/connection references. It covers invalid connections, out-of-range outlet/inlet indices, orphaned patchlines, duplicate IDs, overlapping objects, off-canvas objects, and unknown object classes.
215
+
28
216
  - **Linting runs automatically on `save()`**: error-severity findings (bad connections, out-of-range ports, orphaned lines, duplicate IDs) are logged. Pass `Patcher(strict=True)` to raise `InvalidPatchError` on any error instead. Layout warnings (overlaps / off-canvas / unknown objects) are left to an explicit `lint()` or `py2max validate` to keep normal saves quiet. This is on by default and non-breaking -- saves still succeed unless `strict=True`.
217
+
29
218
  - Connection validation is now **message-type aware and bidirectional**. The previous check only rejected a signal outlet wired into a non-signal inlet; it now also catches a control outlet (a bang from `metro` / `loadbang` / `button`) wired into an oscillator's signal inlet -- e.g. `metro -> cycle~`, which Max rejects. The rules are deliberately conservative (ambiguous cases and maxref-unknown objects are allowed) so on-by-default checking never rejects a valid patch; notably a bang into `adsr~` (a legitimate envelope trigger) is *not* flagged.
219
+
30
220
  - Port typing is now modeled in `py2max/maxref/porttypes.py`, normalizing maxref's placeholder control types (`OUTLET_TYPE` / `INLET_TYPE`) into message kinds, and resolving **argument-dependent port counts** that maxref reports as the arg-less default -- both value-scaled (`limi~ 2` -> 2 in/out) and arg-count-scaled (`select a b c` -> 4 outlets, `route`, `pack`/`unpack`, `selector~`/`switch`) -- plus curated overrides for the handful of objects maxref mis-types.
221
+
31
222
  - `py2max validate` (CLI) now reports the full lint result -- errors and warnings with codes -- and exits non-zero on any error.
223
+
32
224
  - A corpus test re-lints every shipped layout example patch and fails on any error, so Max-invalid wiring can no longer ship unnoticed.
225
+
33
226
  - Subpatchers are handled: a subpatcher/bpatcher box's inlet/outlet count is derived from the `inlet` / `outlet` objects it contains (not the maxref default), and `lint()` recurses into nested patchers -- findings inside a subpatcher are reported path-qualified (e.g. `sub-box-id/obj-1`).
227
+
34
228
  - Inlet acceptance is now derived from each object's `<methodlist>` -- its real message vocabulary in Max's own docs -- rather than the placeholder inlet `type` (Cycling '74 ships `INLET_TYPE`/`OUTLET_TYPE` for control ports, so the type attribute alone is useless). This is what distinguishes a bang into `cycle~` (no `bang` method -> rejected) from a bang into `adsr~` (has an `anything` wildcard method -> allowed), replacing hand-curation with data that generalizes to all ~1050 objects that carry method lists. The shipped `bundle.json.gz` was regenerated so no-Max users get the same data (a `test_bundle_method_data_quality` guard prevents a future regeneration from dropping it).
35
229
 
36
230
  ## [0.3.3]
@@ -42,6 +236,7 @@
42
236
  ### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
43
237
 
44
238
  - `GraphLayoutManager` now runs the dimension-aware overlap sweep (`prevent_overlaps`) after placement, so constraint/force engines no longer leave large UI objects (e.g. `scope~` at 130x130) overlapping their neighbours -- matching the guarantee the grid/flow managers already gave.
239
+
45
240
  - The patcher window is grown to enclose the laid-out graph plus a margin, so `optimize_layout()` output opens with the whole graph visible instead of spilling past the default 640x480 canvas (the window never shrinks below the default).
46
241
 
47
242
  ### Fixed: Clustered grid layout squashed object sizes
@@ -65,42 +260,55 @@
65
260
  ### New: Patch editing and removal API
66
261
 
67
262
  - Loading a patch (`Patcher.from_dict` / `load`) now restores all ID-generation state -- object, node, edge, and semantic-ID counters -- so `add_*` calls made after a load no longer collide with existing object IDs. This fixes the headline "edit an existing patch" round-trip, which previously emitted duplicate IDs on the first post-load add.
263
+
68
264
  - Added a removal / editing API with referential-integrity cleanup: `remove_line` / `disconnect`, `remove_box` / `remove`, and `replace`. Removing a box also prunes its dangling patchlines and clears the associated node/edge/index bookkeeping, so no orphaned lines or stale IDs are left behind.
69
265
 
70
266
  ### New: Graph-layout engines as layout managers
71
267
 
72
268
  - Three optional graph-layout backends -- HOLA (`hola-graph`), COLA plus force-directed / geometric layouts (`graph-layout`), and OGDF's layered / force-directed / planar layouts (`ogdf-py`) -- are now selectable as first-class layout managers via `layout="graph:<algo>"` (e.g. `graph:hola`, `graph:cola`, `graph:ogdf-sugiyama`). Because these algorithms need the whole graph, positions are applied on `optimize_layout()` rather than as each box is added, and each box's width/height is preserved so UI objects are not squashed. Eleven algorithms are available: `hola`, `cola`, `sugiyama`, `fruchterman-reingold`, `kamada-kawai`, `spectral`, `circular`, `shell`, `ogdf-sugiyama`, `ogdf-fmmm`, `ogdf-planarization`.
269
+
73
270
  - The engines are lazy-imported inside the manager, so `import py2max` still pulls **zero runtime dependencies**; a missing backend raises a clear error naming the package to install. Install with `pip install "py2max[graph]"` -- the `graph` extra now also bundles `hola-graph` alongside `graph-layout` and `ogdf-py`. Implemented as `GraphLayoutManager` in `py2max/layout/external.py`.
74
271
 
75
272
  ### New: Layout gallery generator and docs page
76
273
 
77
274
  - `scripts/gen_layout_gallery.py` (run via `make gallery`) renders every supported graph layout over one shared sample patch to transparent SVGs under `docs/assets/imgs/`, using py2max's own SVG exporter so the results are directly comparable. Backends that are not installed are skipped rather than failing the run.
275
+
78
276
  - A new published **Layout Gallery** page (`docs/user_guide/layout_gallery.md`, linked in the User Guide navigation) shows the rendered layouts and documents the `graph:<algo>` layout-manager API. The older networkx / graphviz / tsmpy experiments were dropped in favor of the three maintained backends.
277
+
79
278
  - The generator also renders py2max's **built-in** managers (grid, flow, columnar, matrix) via `optimize_layout()` -- no external dependencies -- and the **Layout Managers** guide (`docs/user_guide/layout_managers.md`) now embeds these as inline visuals so each layout strategy can be seen, not just described.
80
279
 
81
280
  ### Fixed: Generation correctness
82
281
 
83
282
  - `add_coll` / `add_dict` / `add_table` no longer discard a caller-supplied `text` argument when `name` is `None` (an operator-precedence bug that let the fallback string win).
283
+
84
284
  - `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
285
+
85
286
  - Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
287
+
86
288
  - Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
289
+
87
290
  - Hand-typed objects that are not in the built-in defaults now get one outlet instead of zero, so they can act as a connection source.
291
+
88
292
  - Load/save round-trip no longer injects `autosave` / `dependency_cache` defaults into nested subpatchers the source patch did not have; patches containing subpatchers now round-trip faithfully.
89
293
 
90
294
  ### Fixed: Layout managers
91
295
 
92
296
  - `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
297
+
93
298
  - Layout optimizers preserve each object's real width and height. UI objects (`scope~`, `dial`, `slider`, `function`, `live.*`, comments) are no longer squashed to text-box size by `optimize_layout()`.
299
+
94
300
  - The managers now share a single directed-graph model (`PatchGraph`) rather than re-deriving adjacency from patchlines in each manager, and object classification no longer carries contradictory category assignments (its context inference uses maxref signal typing with word-boundary matching).
95
301
 
96
302
  ### Improved: maxref bundle loading (lazy and thread-safe)
97
303
 
98
304
  - In bundle mode (no local Max install), the shipped catalog is no longer eagerly materialized into the cache on first access. The name-to-source map still loads once, but each object is now built from the in-memory bundle on demand, so a single-object query no longer constructs all ~1175 entries. A bundle object whose cache slot is empty is re-materialized from the bundle rather than returning nothing.
305
+
99
306
  - The process-wide maxref cache is now thread-safe: an `RLock` guards the lazy refdict / category-map initialization and cache population, so concurrent `Box.help()` / validation lookups no longer race on first load or mutate the cache unsafely.
100
307
 
101
308
  ### Fixed: maxref outlet digests and parser robustness
102
309
 
103
310
  - Outlet `<digest>` text is now extracted symmetrically with inlets. A condition bug previously required the `<outlet>` element to have leading text and then stored that text instead of the digest, so outlet descriptions were dropped for almost every object (~2000 digests across ~1093 objects). `Box.help()` / `get_info()` now report outlet descriptions. The shipped offline `bundle.json.gz` was regenerated so bundle-mode users (Linux/Windows/no-Max) get the corrected data.
311
+
104
312
  - A raw `&` in reference prose no longer drops the whole object on parse. Ampersands that do not begin a valid XML entity are escaped before parsing, hardening against `.maxref.xml` markup variations across Max versions (valid entities like `&amp;`, `&quot;`, `&#181;` are left intact).
105
313
 
106
314
  ### Improved: Cross-platform maxref discovery
@@ -110,20 +318,27 @@
110
318
  ### Improved: SVG preview fidelity
111
319
 
112
320
  - The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
321
+
113
322
  - **Box colors are honored.** Colors set via `Box.set_color` / `apply_theme` (`bgcolor`, `bordercolor`, `textcolor`) are drawn, converted from Max's `[r, g, b, a]` floats to CSS.
323
+
114
324
  - **UI objects get recognizable affordances** rather than an identical rectangle: message boxes draw the right-edge flag notch, `toggle` an X, `button` a circle, number boxes (`flonum` / `number`) the left triangle marker, `dial` a circle with a pointer, and `slider` a thumb bar.
325
+
115
326
  - **Ports resolve from the box's own `numinlets` / `numoutlets`** (what is written to the `.maxpat`) rather than a maxref lookup. Ports therefore render correctly for objects maxref does not know and without a Max install, and patchline endpoints line up with the ports they connect to (both use the same counts).
327
+
116
328
  - `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
117
329
 
118
330
  ### Fixed: Layout classification and overlap resolution
119
331
 
120
332
  - Object classification (matrix / columnar layouts) is now factored into a clear precedence -- curated functional intent, then maxref signal typing for the unknown audio tail, then name patterns -- with the rationale documented. Curated sets must win because functional categories do not map to raw signal I/O (even `cycle~` exposes a signal inlet, so signal typing alone would call it a processor; `adc~` is an input though it is a signal source). The dead, never-effective `_refine_column_assignments_by_flow` no-op and its call site were removed.
333
+
121
334
  - `LayoutManager.prevent_overlaps` now converges. The previous version cached each object's rect before mutating it (so pushes stopped accumulating) and clamped boxes back inside the canvas (re-introducing the overlaps it had just removed), leaving dense layouts overlapping at the 50-iteration cap. It is replaced with a monotone sweep that pushes each object clear of already-placed ones along the axis of least penetration; it converges in a few passes, early-exits when nothing overlaps, and preserves each object's real size.
122
335
 
123
336
  ### Improved: Patch transformers
124
337
 
125
338
  - `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
339
+
126
340
  - The `add-comment` transformer's position is now reachable from the CLI: prefix the value with `above|below|left|right:` to place the comment (e.g. `--apply "add-comment=below:tempo"`), defaulting to `above`. A leading token that is not a position is kept as comment text, so `"note: hi"` is left intact.
341
+
127
342
  - Added two transformers backed by existing APIs: `apply-theme` (apply a named color theme -- `light` / `dark` / `blue` / `high-contrast`) and `scale-positions` (scale every object's x/y position by a factor, sizes unchanged).
128
343
 
129
344
  ### Fixed: Converters module cleanup
@@ -137,12 +352,15 @@
137
352
  ### Removed: MaxRefDB deprecated alias methods
138
353
 
139
354
  - Removed 12 long-deprecated `MaxRefDB` aliases in favor of the canonical API: `populate_from_maxref` / `populate_all_*` -> `populate([category=...])`, `search_objects` -> `search`, `get_objects_by_category` -> `by_category`, `get_all_categories` -> `.categories`, `get_object_count` -> `.count`, and `export_to_json` / `import_from_json` -> `export` / `load`. The database module is already off the import path (lazily loaded, stdlib-only), so this only trims its API surface. Callers, tests, and docs were updated to the canonical names.
355
+
140
356
  - While updating the SQLite demo scripts, also fixed two pre-existing broken examples: `from py2max import MaxRefDB` (correct: `from py2max.maxref import MaxRefDB`) and `create_database(...)` used as a free function (it is `MaxRefDB.create_database(...)`). Both `tests/examples/db/` scripts now run.
141
357
 
142
358
  ### Removed: Obsolete layout experiments and vendored editor assets
143
359
 
144
360
  - Deleted the old graph-layout experiment tests (`tests/test_layout_hola{1,2,3}`, `test_layout_hola_graph`, `test_layout_networkx{1,2}`, `test_layout_nx_graphviz`, `test_layout_nx_orthogonal`, `test_layout_nx_tsmpy`). They exercised backends that are no longer supported (raw adaptagrams, networkx, pygraphviz, tsmpy) or duplicated the new `GraphLayoutManager` coverage, and only ever skipped. The maintained path is covered by `tests/test_layout_graph_manager.py`.
361
+
145
362
  - Removed the vendored browser libraries under `docs/js/` (SVG.js, WebCola, D3) -- reference copies for the interactive editor that moved to the separate `py2max-server` package in 0.3.0 -- and the stale Sphinx build output under `docs/build/` that predated the MkDocs migration and was being copied into the published site.
363
+
146
364
  - Pruned 30 obsolete design notes from the `docs/notes/` dev journal (REPL, SSE/WebSocket live-preview server, and interactive SVG-editor implementation notes), all for features that moved to `py2max-server`. The 13 still-relevant library/journal notes were kept.
147
365
 
148
366
  ## [0.3.1]
@@ -150,8 +368,11 @@
150
368
  ### New: Standalone `gen.codebox~` Support
151
369
 
152
370
  - `Patcher.add_gen_codebox(code)` adds a self-contained `gen.codebox~` object -- a complete gen patch in a single box that lives directly in a regular Max patcher, distinct from the inner `codebox~` (emitted by `add_codebox`) that belongs inside a `gen~`/`rnbo~` subpatcher. This is the form emitted by gen transpilers. Code newlines are normalized to CRLF as Max expects, and `fontname`/`fontsize` default to the monospaced gen style.
371
+
153
372
  - Inlet/outlet counts are derived automatically from the code (the highest `inN` / `outN` references, floor of 1), matching gen's dynamic-I/O semantics. Explicit `numinlets` / `numoutlets` still override.
373
+
154
374
  - Available via the `add()` string shortcut too: `p.add("gen.codebox~ out1 = in1 * 0.5;")`. The shortcut suits single-line / `;`-terminated code; pass multi-line source to `add_gen_codebox()` directly.
375
+
155
376
  - Connection validation for `gen.codebox~` (and `codebox` / `codebox~`) now bound-checks against the box's own declared inlet/outlet counts rather than the static `.maxref.xml` entry, since codebox I/O is code-dependent. This both allows valid connections to/from wider codeboxes (e.g. from a second outlet) and rejects genuinely out-of-range ones.
156
377
 
157
378
  ## [0.3.0]
@@ -161,7 +382,9 @@
161
382
  The browser-based live editor and remote REPL have moved to a separate companion package, [`py2max-server`](https://github.com/shakfu/py2max-server), so the core library stays small, offline, and dependency-free.
162
383
 
163
384
  - Removed `Patcher.serve()` and the `py2max serve` / `py2max repl` CLI commands; those CLI subcommands now print a pointer to `py2max-server`.
385
+
164
386
  - Removed the `[server]` optional-dependency extra (`websockets`, `ptpython`) and the bundled browser assets (`py2max/static/`).
387
+
165
388
  - Install the server features with `pip install py2max-server` and use `py2max-server serve <patch>` / `py2max-server repl …`. The remote REPL now requires token authentication (passed via `--token` or `PY2MAX_REPL_TOKEN`).
166
389
 
167
390
  ### New: `Patcher.encapsulate()`
@@ -171,6 +394,7 @@ The browser-based live editor and remote REPL have moved to a separate companion
171
394
  ### New: Preset / `pattrstorage` Scaffolding
172
395
 
173
396
  - `Patcher.add_pattrstorage(name)`, `Patcher.add_autopattr()`, and `Patcher.add_preset_system(name)` (which adds both and wires `autopattr` -> `pattrstorage`) scaffold a Max preset system. Any object with a scripting name (`varname`) or `parameter_enable=1` participates.
397
+
174
398
  - `Patcher.enable_parameter(box, longname, shortname="", ptype=0, initial=None)` turns an existing UI box into a Max parameter (sets `parameter_enable` and the `saved_attribute_attributes`), so it participates in presets and, in a Max for Live device, appears as an automatable parameter.
175
399
 
176
400
  ### New: Keyword-Attribute Validation (`validate_attrs`)
@@ -180,6 +404,7 @@ The browser-based live editor and remote REPL have moved to a separate companion
180
404
  ### New: Multichannel (`mc.`) / Polyphony Helpers
181
405
 
182
406
  - `Patcher.add_mc(text, chans=None)` adds a multichannel object, prefixing `mc.` and appending `@chans` (e.g. `add_mc("cycle~ 440", chans=4)` -> `mc.cycle~ 440 @chans 4`).
407
+
183
408
  - `Patcher.add_poly(target, voices=1)` adds a `poly~` object hosting N voices of a target patch.
184
409
 
185
410
  ### Improved: SVG Export (Max-faithful preview)
@@ -193,7 +418,9 @@ The browser-based live editor and remote REPL have moved to a separate companion
193
418
  ### New: Color / Theme Helpers
194
419
 
195
420
  - `Box.set_color(bg=..., text=..., border=...)` sets a box's `bgcolor`/`textcolor`/`bordercolor`; each accepts a named color (e.g. `"red"`), a hex string (`"#ff8800"`), or an `[r, g, b(, a)]` float sequence. Returns the box for chaining.
421
+
196
422
  - `Patcher.apply_theme(theme)` applies a color theme to every box (recursing into subpatchers). Built-in themes: `"light"`, `"dark"`, `"blue"`, `"high-contrast"`; or pass a dict of `bg`/`text`/`border` colors.
423
+
197
424
  - `py2max.core.colors` exposes the `MAX_COLORS` named palette and `resolve_color()`.
198
425
 
199
426
  ### Security
@@ -215,14 +442,19 @@ The browser-based live editor and remote REPL have moved to a separate companion
215
442
  ### Fixed
216
443
 
217
444
  - Object-name resolution (used by connection validation and object classification) now reads the box `text` property, so it resolves correctly for boxes loaded from a file. Previously it inspected only programmatic kwargs and returned `newobj` for loaded boxes.
445
+
218
446
  - `Box.oid` now returns the trailing numeric part of any id (e.g. `cycle_1` -> 1) instead of raising `ValueError` under `semantic_ids=True`.
447
+
219
448
  - The `py2max` CLI now reports all `Py2MaxError`s (not just `InvalidConnectionError`) as a clean error message instead of leaking a traceback.
449
+
220
450
  - Fixed an `inital` -> `initial` keyword typo in the simple-synthesis tutorial.
221
451
 
222
452
  ### Testing & Tooling
223
453
 
224
454
  - The test suite is now hermetic: a `conftest.py` autouse fixture isolates each test in a temporary working directory, so relative `outputs/` writes no longer accumulate in the repo. Fixture reads are anchored at the test file.
455
+
225
456
  - Promoted the `.amxd` byte-for-byte fixtures from the gitignored `outputs/` into tracked `tests/data/`, so that verification runs in CI and on fresh checkouts instead of only on the author's machine.
457
+
226
458
  - Repo-wide `ruff` lint and format cleanup.
227
459
 
228
460
  ### New: Max for Live Support (`py2max.m4l`)
@@ -230,20 +462,27 @@ The browser-based live editor and remote REPL have moved to a separate companion
230
462
  Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/notes/amxd.md`](https://github.com/shakfu/py2max/blob/main/docs/notes/amxd.md) for the on-disk format, embedded-project block, and verification details.
231
463
 
232
464
  - **`.amxd` read/write**: byte-for-byte compatible with Max-exported devices; verified against real fixtures and end-to-end in Live 12.
465
+
233
466
  - **Device-type discrimination**: Audio Effect / Instrument / MIDI Effect via `Patcher(device_type=...)` or the `pack_amxd` / `write_amxd` `device_type` argument.
467
+
234
468
  - **Presentation-mode helpers**: `Patcher.enable_presentation(devicewidth=...)`, `Patcher.enforce_integer_coords()`, `Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects, rounds fractional coords with a warning).
469
+
235
470
  - `Patcher.save()` / `Patcher.from_file()` auto-detect the `.amxd` extension; `.maxpat` path is unchanged.
236
471
 
237
472
  ### Changed: M4L Module Layout & Imports
238
473
 
239
474
  - All M4L code (binary format + presentation helpers) lives in a single module `py2max/m4l.py`. Previously briefly split as `py2max/amxd.py`.
475
+
240
476
  - M4L symbols are reachable only via `from py2max.m4l import …`; nothing is re-exported from the top-level `py2max` namespace.
241
477
 
242
478
  ### New: Prebuilt MaxRef Bundle (Linux Support)
243
479
 
244
480
  - Ship `py2max/maxref/data/bundle.json.gz` in the wheel (1175 objects, ~1 MiB compressed, ~7 MiB raw).
481
+
245
482
  - `MaxRefCache._get_refdict()` falls back to the bundle when no local Max installation is found, pre-seeding the parser cache so `Box.help()`, `get_inlet_count`, `get_outlet_count`, and connection validation work identically on Linux.
483
+
246
484
  - Regenerate with `uv run python scripts/build_maxref_bundle.py` on a machine with Max installed; commit the result.
485
+
247
486
  - Bundle stores full parsed data (methods, attributes, inlets/outlets, digests, descriptions) — not a trimmed subset — so introspection parity with macOS/Windows is preserved.
248
487
 
249
488
  ## [0.2.1] - 2026-01-11
@@ -251,55 +490,81 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
251
490
  ### New: Dagre Layout Algorithm
252
491
 
253
492
  - Added Dagre (Directed Acyclic Graph) as third layout algorithm option alongside WebCola and ELK
493
+
254
494
  - Integrated `dagre-bundle.js` combining graphlib with require shim for browser compatibility
495
+
255
496
  - Added Dagre-specific controls: Ranker (network-simplex, longest-path, tight-tree) and Align options
497
+
256
498
  - Supports all flow directions: top-bottom, bottom-top, left-right, right-left
257
499
 
258
500
  ### Improved: Interactive Editor Visualization
259
501
 
260
502
  - **ViewBox Scaling**: Dynamic padding (10% of content, min 30px, max 100px) with aspect ratio preservation
503
+
261
504
  - **Port Position Safety**: Added bounds checking with `safeIndex` clamping to prevent invalid port positions
505
+
262
506
  - **Patchline Animation**: Added `animatePatchlines()` method for smooth patchline transitions during layout
507
+
263
508
  - **Layout Centering**: Added `centerLayout()` helper method - all three algorithms now center content within canvas
509
+
264
510
  - **Delta Updates**: Position updates now send only changed box data instead of full patcher state
511
+
265
512
  - Added `updateBoxPosition()` for efficient single-box DOM updates
513
+
266
514
  - Added `updateConnectedLines()` to update patchlines without full re-render
515
+
267
516
  - Significantly reduces bandwidth during drag operations
268
517
 
269
518
  ### Improved: FlowLayoutManager
270
519
 
271
520
  - **Line Crossing Minimization**: Added `_minimize_crossings()` method using barycenter heuristic
521
+
272
522
  - Objects within each level are reordered based on average position of connected objects in previous level
523
+
273
524
  - Reduces visual line crossings for cleaner layouts
525
+
274
526
  - **Negative Position Prevention**: Added bounds clamping and auto-scaling when content exceeds available space
527
+
275
528
  - **Incremental Layout**: Supports `optimize_layout(changed_objects)` for efficient partial updates
276
529
 
277
530
  ### Improved: GridLayoutManager
278
531
 
279
532
  - Fixed integer division to float division for consistent cluster positioning
533
+
280
534
  - Now uses consistent float spacing within clusters
535
+
281
536
  - **Incremental Layout**: Supports `optimize_layout(changed_objects)` for efficient partial updates
282
537
 
283
538
  ### Improved: WebSocket Server Security
284
539
 
285
540
  - **Input Validation**: Added comprehensive schema-based message validation
541
+
286
542
  - `MESSAGE_SCHEMAS` defines required fields and types for each message type
543
+
287
544
  - `MAX_STRING_LENGTHS` prevents abuse (256 chars for IDs, 10000 for text, 4096 for filepaths)
545
+
288
546
  - `COORDINATE_BOUNDS` validates positions (-100000 to 100000)
547
+
289
548
  - Checks for control characters in strings
549
+
290
550
  - Validates optional fields (outlet/inlet indices 0-255)
551
+
291
552
  - Validation errors sent back to client as error messages
292
553
 
293
554
  ### New: Save As Dialog
294
555
 
295
556
  - Added `save_as_required` message type when patcher has no filepath
557
+
296
558
  - Added `handle_save_as()` handler for saving with specified filepath
559
+
297
560
  - Added `showSaveAsDialog()` in JavaScript with filename prompt
561
+
298
562
  - Automatically adds `.maxpat` extension if not provided
299
563
 
300
564
  ### Fixed: ELK Layout
301
565
 
302
566
  - Fixed "Referenced shape does not exist" errors by validating edges before creating ports
567
+
303
568
  - Ports now created based on actual connections, not just declared counts
304
569
 
305
570
  ### Fixed: Static File Paths
@@ -309,12 +574,19 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
309
574
  ### Improved: Base LayoutManager
310
575
 
311
576
  - Added `prevent_overlaps()` method for iterative overlap prevention
577
+
312
578
  - **Incremental Layout System**: Added smart layout optimization that only repositions affected objects
579
+
313
580
  - `optimize_layout(changed_objects)` accepts optional set of changed object IDs
581
+
314
582
  - `should_use_incremental()` determines when to use incremental vs full layout (30% threshold)
583
+
315
584
  - `get_affected_objects()` finds changed objects plus their connected neighbors
585
+
316
586
  - `_incremental_layout()` repositions only affected objects using spiral search
587
+
317
588
  - `_find_non_overlapping_position()` finds nearby positions that don't overlap with fixed objects
589
+
318
590
  - `_full_layout()` for complete layout recalculation (subclasses override)
319
591
 
320
592
  ## [0.2.0]
@@ -322,21 +594,29 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
322
594
  ### Updated: Optional Layout Dependencies
323
595
 
324
596
  - Updated `pycola` dependency to `graph-layout` package (<https://github.com/shakfu/graph-layout>)
597
+
325
598
  - Renamed test file from `test_layout_pycola.py` to `test_layout_graph_layout.py`
599
+
326
600
  - Updated API to use `ColaLayoutAdapter` from `graph_layout` module
327
601
 
328
602
  - Updated `pyhola` dependency to `hola-graph` package (<https://github.com/shakfu/hola-graph>)
603
+
329
604
  - Renamed test file from `test_layout_pyhola.py` to `test_layout_hola_graph.py`
605
+
330
606
  - Updated imports to use `hola_graph._core` module
331
607
 
332
608
  - Fixed `test_layout_networkx2.py` to properly check for `pygraphviz` dependency
609
+
333
610
  - Test now correctly skips when pygraphviz is not installed
334
611
 
335
612
  ### Simplified: Optional Dependencies
336
613
 
337
614
  - Consolidated optional dependencies in `pyproject.toml` to single `server` option
615
+
338
616
  - Removed `repl` and `all` options
617
+
339
618
  - `server` now includes both `websockets` and `ptpython`
619
+
340
620
  - Install with: `pip install py2max[server]`
341
621
 
342
622
  ### New: Interactive Editor - Advanced Layout with SVG.js, WebCola, and D3.js
@@ -348,16 +628,25 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
348
628
  - Added interactive auto-layout controls panel with real-time parameter adjustment
349
629
 
350
630
  - Added 5 adjustable layout parameters via sliders and controls:
631
+
351
632
  - **Link Distance** (50-300): Controls spacing between connected objects
633
+
352
634
  - **Iterations** (10-200): Controls layout quality and convergence
635
+
353
636
  - **Canvas Width** (400-1600): Adjustable layout area width
637
+
354
638
  - **Canvas Height** (300-1200): Adjustable layout area height
639
+
355
640
  - **Avoid Overlaps** (checkbox): Toggle automatic overlap prevention
356
641
 
357
642
  - Added constraint-based layout system with 4 presets:
643
+
358
644
  - **None**: Natural force-directed layout without alignment constraints
645
+
359
646
  - **Horizontal Flow**: Aligns objects in horizontal rows (left-to-right signal flow)
647
+
360
648
  - **Vertical Flow**: Aligns objects in vertical columns (top-to-bottom signal flow)
649
+
361
650
  - **Grid**: Strict grid alignment with both row and column constraints
362
651
 
363
652
  - Added smooth SVG.js animations (500ms ease-in-out) for layout transitions
@@ -371,50 +660,75 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
371
660
  **SVG.js Implementation:**
372
661
 
373
662
  - Refactored all SVG rendering to use SVG.js declarative API instead of native DOM manipulation
663
+
374
664
  - `initializeSVG()`: Creates SVG canvas and layer groups using SVG.js
665
+
375
666
  - `createBox()`: Renders boxes with rectangles, text, and clipping paths using SVG.js
667
+
376
668
  - `createLine()`: Renders connection lines with hitboxes using SVG.js
669
+
377
670
  - `addPorts()`: Renders inlet/outlet circles using SVG.js
671
+
378
672
  - `autoLayout()`: Animates box movements using SVG.js transforms
379
673
 
380
674
  **WebCola Integration:**
381
675
 
382
676
  - Force-directed graph layout with configurable parameters
677
+
383
678
  - Constraint-based positioning using alignment constraints
679
+
384
680
  - Automatic overlap avoidance with adjustable node dimensions
681
+
385
682
  - Handles disconnected graph components gracefully
683
+
386
684
  - Jaccard link lengths for natural connection spacing
387
685
 
388
686
  **Constraint System:**
389
687
 
390
688
  - Automatic constraint generation based on object proximity (50px threshold)
689
+
391
690
  - Alignment constraints for horizontal rows (Y-axis alignment)
691
+
392
692
  - Alignment constraints for vertical columns (X-axis alignment)
693
+
393
694
  - Grid constraints combining both row and column alignment
695
+
394
696
  - Real-time constraint application with visual feedback
395
697
 
396
698
  **Documentation:**
397
699
 
398
700
  - Added comprehensive `docs/LIBRARIES_INTEGRATION.md` (518 lines)
701
+
399
702
  - Detailed parameter descriptions and effects
703
+
400
704
  - Constraint preset usage examples
705
+
401
706
  - Testing procedures and expected behavior
707
+
402
708
  - Code examples and API documentation
709
+
403
710
  - Performance considerations for different patch sizes
404
711
 
405
712
  **Demo Scripts:**
406
713
 
407
714
  - Added `examples/auto_layout_demo.py`: Complex synthesizer with randomized positions (13 objects, 16 connections)
715
+
408
716
  - Hierarchical layout demo: Tree structure with multiple processing layers (12 objects)
409
717
 
410
718
  **Benefits:**
411
719
 
412
720
  - Professional animated transitions for all layout operations
721
+
413
722
  - Interactive experimentation with layout parameters
723
+
414
724
  - Structured layouts matching typical Max patch patterns
725
+
415
726
  - Clean, maintainable SVG.js codebase
727
+
416
728
  - Four layout presets for different use cases
729
+
417
730
  - Real-time visual feedback
731
+
418
732
  - Minimal overhead (234KB total: D3 + SVG.js + WebCola, minified)
419
733
 
420
734
  **Example Usage:**
@@ -435,44 +749,63 @@ py2max serve outputs/auto_layout_demo.maxpat
435
749
  ### New: Interactive Editor - Nested Patcher Navigation
436
750
 
437
751
  - Added full nested patcher (subpatcher) navigation support in interactive editor
752
+
438
753
  - Double-click on subpatcher boxes (blue dashed border) to navigate into them
754
+
439
755
  - Navigate back using "Parent" button or ESC key
756
+
440
757
  - Breadcrumb navigation displays current location (e.g., "Main / Oscillator / Envelope")
758
+
441
759
  - Subpatcher boxes are fully interactive: draggable, connectable, deletable
760
+
442
761
  - Visual distinction: subpatcher boxes have blue dashed borders and bold blue text
762
+
443
763
  - Event delegation for reliable double-click detection even with dynamic DOM updates
764
+
444
765
  - Automatic parent reference restoration when loading patches from files
445
766
 
446
767
  **Server-Side Changes:**
447
768
 
448
769
  - Modified `get_patcher_state_json()` to include `has_subpatcher` flag and `patcher_path` breadcrumb
770
+
449
771
  - Added `handle_navigate_to_subpatcher()`, `handle_navigate_to_parent()`, `handle_navigate_to_root()` handlers
772
+
450
773
  - Fixed inlet/outlet count detection to use `numinlets`/`numoutlets` attributes from loaded files
774
+
451
775
  - Handler now tracks both `root_patcher` (for saving) and `patcher` (current view)
452
776
 
453
777
  **Client-Side Changes:**
454
778
 
455
779
  - Added breadcrumb UI showing patcher hierarchy
780
+
456
781
  - Implemented event delegation for double-click handling on dynamically created boxes
782
+
457
783
  - Fixed object positioning by flattening `patching_rect` into `x`, `y`, `width`, `height`
784
+
458
785
  - CSS styling for subpatcher boxes with distinct visual appearance
786
+
459
787
  - ESC key navigation support
460
788
 
461
789
  **Core Changes:**
462
790
 
463
791
  - Modified `Patcher.from_dict()` to set `_parent` references for nested subpatchers when loading from files
792
+
464
793
  - Ensures bidirectional parent-child relationships for proper navigation
465
794
 
466
795
  **Tests:**
467
796
 
468
797
  - Added 14 comprehensive tests in `tests/test_nested_patchers.py`
798
+
469
799
  - All tests passing (326 passed, 14 skipped)
470
800
 
471
801
  **Demo:**
472
802
 
473
803
  - Added `examples/nested_patcher_demo.py` with three demonstration patches:
804
+
474
805
  - Synthesizer with nested envelope subpatcher
806
+
475
807
  - Effects chain with parallel subpatchers
808
+
476
809
  - Deeply nested hierarchy (6 levels)
477
810
 
478
811
  ### New: SVG Preview Feature
@@ -484,8 +817,11 @@ py2max serve outputs/auto_layout_demo.maxpat
484
817
  - Added `export_svg()` and `export_svg_string()` functions for programmatic SVG generation
485
818
 
486
819
  - Added SVG rendering for boxes with type-specific styling:
820
+
487
821
  - Regular objects: Light gray fill
822
+
488
823
  - Comments: Yellow fill (#ffffd0)
824
+
489
825
  - Messages: Medium gray fill
490
826
 
491
827
  - Added patchline rendering with correct inlet/outlet connection points
@@ -550,10 +886,15 @@ svg_content = export_svg_string(p, show_ports=True)
550
886
  **Benefits:**
551
887
 
552
888
  - No Max installation required for visual validation
889
+
553
890
  - High-quality, scalable vector graphics
891
+
554
892
  - Works with all py2max layout managers
893
+
555
894
  - Perfect for CI/CD, documentation, and version control
895
+
556
896
  - Pure Python implementation with no binary dependencies
897
+
557
898
  - Viewable in any web browser
558
899
 
559
900
  ### New: SQLite Database Support
@@ -593,24 +934,39 @@ svg_content = export_svg_string(p, show_ports=True)
593
934
  **Python API Improvements:**
594
935
 
595
936
  - Added Pythonic properties: `.count`, `.categories`, `.objects` for cleaner access
937
+
596
938
  - Added magic methods: `len(db)`, `'obj' in db`, `db['obj']`, `repr(db)` for natural Python usage
939
+
597
940
  - Added simplified methods: `populate()`, `search()`, `by_category()`, `export()`, `load()` with cleaner naming
941
+
598
942
  - Added `summary()` method for database statistics with category breakdown
943
+
599
944
  - Maintained full backward compatibility with deprecated methods
945
+
600
946
  - All 18 database tests pass
601
947
 
602
948
  **CLI Improvements:**
603
949
 
604
950
  - Added comprehensive `py2max db` subcommand with 7 operations:
951
+
605
952
  - `db create` - Create new databases with optional category filtering
953
+
606
954
  - `db populate` - Add objects to existing databases
955
+
607
956
  - `db info` - Show database information with summary and listing options
957
+
608
958
  - `db search` - Search objects by text or category with verbose mode
959
+
609
960
  - `db query` - Get detailed object information (JSON, dict, or human-readable)
961
+
610
962
  - `db export` - Export database to JSON
963
+
611
964
  - `db import` - Import JSON data into database
965
+
612
966
  - Updated `convert maxref-to-sqlite` to use MaxRefDB internally
967
+
613
968
  - Added 7 new CLI tests covering all db subcommands
969
+
614
970
  - All 272 tests pass (258 passed, 14 skipped)
615
971
 
616
972
  **Example Usage:**
@@ -647,31 +1003,41 @@ py2max db cache clear
647
1003
  MaxRefDB now automatically creates and populates a cache database on first use:
648
1004
 
649
1005
  - **macOS**: `~/Library/Caches/py2max/maxref.db`
1006
+
650
1007
  - **Linux**: `~/.cache/py2max/maxref.db`
1008
+
651
1009
  - **Windows**: `~/AppData/Local/py2max/Cache/maxref.db`
652
1010
 
653
1011
  **Benefits:**
654
1012
 
655
1013
  - One-time population of all 1157 Max objects
1014
+
656
1015
  - Instant access on subsequent use
1016
+
657
1017
  - No manual setup required
1018
+
658
1019
  - Platform-appropriate cache location
659
1020
 
660
1021
  **New Static Methods:**
661
1022
 
662
1023
  - `MaxRefDB.get_cache_dir()` - Get platform-specific cache directory
1024
+
663
1025
  - `MaxRefDB.get_default_db_path()` - Get default database path
664
1026
 
665
1027
  **Updated API:**
666
1028
 
667
1029
  - `MaxRefDB()` - Now uses cache by default
1030
+
668
1031
  - `MaxRefDB(db_path, auto_populate=True)` - Control auto-population
1032
+
669
1033
  - `MaxRefDB(':memory:')` - In-memory database (no caching)
670
1034
 
671
1035
  **New CLI Commands:**
672
1036
 
673
1037
  - `py2max db cache location` - Show cache location and status
1038
+
674
1039
  - `py2max db cache init` - Manually initialize cache
1040
+
675
1041
  - `py2max db cache clear` - Clear cache database
676
1042
 
677
1043
  **Example Usage:**