py2max 0.3.6__tar.gz → 0.4.1__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 (194) hide show
  1. {py2max-0.3.6 → py2max-0.4.1}/CHANGELOG.md +379 -0
  2. {py2max-0.3.6 → py2max-0.4.1}/PKG-INFO +54 -8
  3. {py2max-0.3.6 → py2max-0.4.1}/README.md +53 -7
  4. {py2max-0.3.6 → py2max-0.4.1}/py2max/__init__.py +18 -2
  5. {py2max-0.3.6 → py2max-0.4.1}/py2max/cli.py +17 -0
  6. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/abstract.py +1 -0
  7. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/box.py +79 -10
  8. py2max-0.4.1/py2max/core/common.py +56 -0
  9. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/factory.py +182 -48
  10. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/patcher.py +29 -8
  11. py2max-0.4.1/py2max/core/props.py +1748 -0
  12. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/serialization.py +27 -2
  13. py2max-0.4.1/py2max/data/js2max/js2max.js +2017 -0
  14. py2max-0.4.1/py2max/data/js2max/js2max.v8.js +2620 -0
  15. py2max-0.4.1/py2max/js2max_runtime.py +123 -0
  16. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/matrix.py +10 -2
  17. {py2max-0.3.6 → py2max-0.4.1}/py2max/log.py +124 -52
  18. {py2max-0.3.6 → py2max-0.4.1}/py2max/m4l.py +27 -17
  19. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/db.py +44 -13
  20. py2max-0.4.1/pyproject.toml +97 -0
  21. py2max-0.3.6/pyproject.toml → py2max-0.4.1/pyproject.toml.orig +1 -1
  22. {py2max-0.3.6 → py2max-0.4.1}/tests/conftest.py +25 -2
  23. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/README.md +10 -1
  24. py2max-0.4.1/tests/test_add.py +176 -0
  25. py2max-0.4.1/tests/test_box_props.py +186 -0
  26. {py2max-0.3.6 → py2max-0.4.1}/tests/test_comment.py +15 -0
  27. {py2max-0.3.6 → py2max-0.4.1}/tests/test_db.py +76 -0
  28. py2max-0.4.1/tests/test_js2max_runtime.py +258 -0
  29. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_matrix.py +62 -0
  30. py2max-0.4.1/tests/test_loaded_rects.py +131 -0
  31. py2max-0.4.1/tests/test_logging.py +189 -0
  32. py2max-0.4.1/tests/test_param.py +107 -0
  33. py2max-0.4.1/tests/test_serialization.py +130 -0
  34. py2max-0.4.1/tests/test_single_file.py +369 -0
  35. py2max-0.4.1/tests/test_subpatch.py +98 -0
  36. py2max-0.3.6/py2max/core/common.py +0 -19
  37. py2max-0.3.6/tests/test_add.py +0 -65
  38. py2max-0.3.6/tests/test_param.py +0 -36
  39. py2max-0.3.6/tests/test_subpatch.py +0 -34
  40. {py2max-0.3.6 → py2max-0.4.1}/LICENSE +0 -0
  41. {py2max-0.3.6 → py2max-0.4.1}/py2max/__main__.py +0 -0
  42. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/__init__.py +0 -0
  43. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/colors.py +0 -0
  44. {py2max-0.3.6 → py2max-0.4.1}/py2max/core/patchline.py +0 -0
  45. {py2max-0.3.6 → py2max-0.4.1}/py2max/exceptions.py +0 -0
  46. {py2max-0.3.6 → py2max-0.4.1}/py2max/export/__init__.py +0 -0
  47. {py2max-0.3.6 → py2max-0.4.1}/py2max/export/converters.py +0 -0
  48. {py2max-0.3.6 → py2max-0.4.1}/py2max/export/svg.py +0 -0
  49. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/__init__.py +0 -0
  50. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/base.py +0 -0
  51. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/external.py +0 -0
  52. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/flow.py +0 -0
  53. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/graph.py +0 -0
  54. {py2max-0.3.6 → py2max-0.4.1}/py2max/layout/grid.py +0 -0
  55. {py2max-0.3.6 → py2max-0.4.1}/py2max/lint.py +0 -0
  56. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/__init__.py +0 -0
  57. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/category.py +0 -0
  58. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/data/bundle.json.gz +0 -0
  59. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/legacy.py +0 -0
  60. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/parser.py +0 -0
  61. {py2max-0.3.6 → py2max-0.4.1}/py2max/maxref/porttypes.py +0 -0
  62. {py2max-0.3.6 → py2max-0.4.1}/py2max/py.typed +0 -0
  63. {py2max-0.3.6 → py2max-0.4.1}/py2max/transformers.py +0 -0
  64. {py2max-0.3.6 → py2max-0.4.1}/py2max/utils.py +0 -0
  65. {py2max-0.3.6 → py2max-0.4.1}/tests/__init__.py +0 -0
  66. {py2max-0.3.6 → py2max-0.4.1}/tests/data/complex.maxpat +0 -0
  67. {py2max-0.3.6 → py2max-0.4.1}/tests/data/desc.maxpat +0 -0
  68. {py2max-0.3.6 → py2max-0.4.1}/tests/data/empty.maxpat +0 -0
  69. {py2max-0.3.6 → py2max-0.4.1}/tests/data/mydevice.amxd +0 -0
  70. {py2max-0.3.6 → py2max-0.4.1}/tests/data/mydevice2.amxd +0 -0
  71. {py2max-0.3.6 → py2max-0.4.1}/tests/data/nested.maxpat +0 -0
  72. {py2max-0.3.6 → py2max-0.4.1}/tests/data/simple.maxpat +0 -0
  73. {py2max-0.3.6 → py2max-0.4.1}/tests/data/tabular.maxpat +0 -0
  74. {py2max-0.3.6 → py2max-0.4.1}/tests/data/umenu.maxref.xml +0 -0
  75. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/advanced/connection_patterns.py +0 -0
  76. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/advanced/custom_extensions.py +0 -0
  77. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/advanced/data_containers.py +0 -0
  78. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/advanced/error_handling.py +0 -0
  79. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/advanced/performance_optimization.py +0 -0
  80. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/advanced/subpatchers.py +0 -0
  81. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/api/box_api_examples.py +0 -0
  82. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/api/patcher_api_examples.py +0 -0
  83. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/auto_layout_demo.py +0 -0
  84. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/db/category_db_demo.py +0 -0
  85. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/db/maxref_db_demo.py +0 -0
  86. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/layout/columnar_layout_examples.py +0 -0
  87. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/layout/flow_layout_examples.py +0 -0
  88. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/layout/grid_layout_examples.py +0 -0
  89. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/layout/matrix_layout_examples.py +0 -0
  90. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/basic_synth.maxpat +0 -0
  91. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/basic_synth.svg +0 -0
  92. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/complex_synth.maxpat +0 -0
  93. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/complex_synth.svg +0 -0
  94. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/flow_layout.maxpat +0 -0
  95. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/flow_layout.svg +0 -0
  96. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/grid_layout.maxpat +0 -0
  97. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/grid_layout.svg +0 -0
  98. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  99. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/horizontal_layout.svg +0 -0
  100. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/styled_no_ports.svg +0 -0
  101. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/styled_no_title.svg +0 -0
  102. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/styled_patch.maxpat +0 -0
  103. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/styled_with_ports.svg +0 -0
  104. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/svg_preview_demo.py +0 -0
  105. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/vertical_layout.maxpat +0 -0
  106. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/vertical_layout.svg +0 -0
  107. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/workflow_demo.maxpat +0 -0
  108. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/preview/workflow_demo.svg +0 -0
  109. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/quickstart/basic_patch.py +0 -0
  110. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/quickstart/layout_examples.py +0 -0
  111. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/tutorial/generative_music.py +0 -0
  112. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/tutorial/interactive_controller.py +0 -0
  113. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  114. {py2max-0.3.6 → py2max-0.4.1}/tests/examples/tutorial/simple_synthesis.py +0 -0
  115. {py2max-0.3.6 → py2max-0.4.1}/tests/graphs/random/v30e33.tglf +0 -0
  116. {py2max-0.3.6 → py2max-0.4.1}/tests/test_abstract_coverage.py +0 -0
  117. {py2max-0.3.6 → py2max-0.4.1}/tests/test_abstraction.py +0 -0
  118. {py2max-0.3.6 → py2max-0.4.1}/tests/test_amxd.py +0 -0
  119. {py2max-0.3.6 → py2max-0.4.1}/tests/test_attrui.py +0 -0
  120. {py2max-0.3.6 → py2max-0.4.1}/tests/test_basic.py +0 -0
  121. {py2max-0.3.6 → py2max-0.4.1}/tests/test_beap.py +0 -0
  122. {py2max-0.3.6 → py2max-0.4.1}/tests/test_bpatcher.py +0 -0
  123. {py2max-0.3.6 → py2max-0.4.1}/tests/test_cli.py +0 -0
  124. {py2max-0.3.6 → py2max-0.4.1}/tests/test_coll.py +0 -0
  125. {py2max-0.3.6 → py2max-0.4.1}/tests/test_colors.py +0 -0
  126. {py2max-0.3.6 → py2max-0.4.1}/tests/test_colors_theme.py +0 -0
  127. {py2max-0.3.6 → py2max-0.4.1}/tests/test_connection_validation.py +0 -0
  128. {py2max-0.3.6 → py2max-0.4.1}/tests/test_converters.py +0 -0
  129. {py2max-0.3.6 → py2max-0.4.1}/tests/test_core_coverage.py +0 -0
  130. {py2max-0.3.6 → py2max-0.4.1}/tests/test_defaults.py +0 -0
  131. {py2max-0.3.6 → py2max-0.4.1}/tests/test_dict.py +0 -0
  132. {py2max-0.3.6 → py2max-0.4.1}/tests/test_edit_operations.py +0 -0
  133. {py2max-0.3.6 → py2max-0.4.1}/tests/test_encapsulate.py +0 -0
  134. {py2max-0.3.6 → py2max-0.4.1}/tests/test_error_handling.py +0 -0
  135. {py2max-0.3.6 → py2max-0.4.1}/tests/test_examples.py +0 -0
  136. {py2max-0.3.6 → py2max-0.4.1}/tests/test_ezdac.py +0 -0
  137. {py2max-0.3.6 → py2max-0.4.1}/tests/test_gen.py +0 -0
  138. {py2max-0.3.6 → py2max-0.4.1}/tests/test_group.py +0 -0
  139. {py2max-0.3.6 → py2max-0.4.1}/tests/test_itable.py +0 -0
  140. {py2max-0.3.6 → py2max-0.4.1}/tests/test_js.py +0 -0
  141. {py2max-0.3.6 → py2max-0.4.1}/tests/test_kwds_filter.py +0 -0
  142. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout.py +0 -0
  143. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_box_dims.py +0 -0
  144. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_builtins.py +0 -0
  145. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_coverage.py +0 -0
  146. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_flow.py +0 -0
  147. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_graph.py +0 -0
  148. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_graph_layout.py +0 -0
  149. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_graph_manager.py +0 -0
  150. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_overlaps.py +0 -0
  151. {py2max-0.3.6 → py2max-0.4.1}/tests/test_layout_vertical.py +0 -0
  152. {py2max-0.3.6 → py2max-0.4.1}/tests/test_linking.py +0 -0
  153. {py2max-0.3.6 → py2max-0.4.1}/tests/test_lint.py +0 -0
  154. {py2max-0.3.6 → py2max-0.4.1}/tests/test_m4l.py +0 -0
  155. {py2max-0.3.6 → py2max-0.4.1}/tests/test_maxref.py +0 -0
  156. {py2max-0.3.6 → py2max-0.4.1}/tests/test_maxref_bundle.py +0 -0
  157. {py2max-0.3.6 → py2max-0.4.1}/tests/test_maxref_lazy.py +0 -0
  158. {py2max-0.3.6 → py2max-0.4.1}/tests/test_maxref_parser.py +0 -0
  159. {py2max-0.3.6 → py2max-0.4.1}/tests/test_maxref_refpages.py +0 -0
  160. {py2max-0.3.6 → py2max-0.4.1}/tests/test_mc_cycle.py +0 -0
  161. {py2max-0.3.6 → py2max-0.4.1}/tests/test_mc_poly.py +0 -0
  162. {py2max-0.3.6 → py2max-0.4.1}/tests/test_message.py +0 -0
  163. {py2max-0.3.6 → py2max-0.4.1}/tests/test_mypatch.py +0 -0
  164. {py2max-0.3.6 → py2max-0.4.1}/tests/test_nested.py +0 -0
  165. {py2max-0.3.6 → py2max-0.4.1}/tests/test_nested_patchers.py +0 -0
  166. {py2max-0.3.6 → py2max-0.4.1}/tests/test_number_tilde.py +0 -0
  167. {py2max-0.3.6 → py2max-0.4.1}/tests/test_numbers.py +0 -0
  168. {py2max-0.3.6 → py2max-0.4.1}/tests/test_param_placement.py +0 -0
  169. {py2max-0.3.6 → py2max-0.4.1}/tests/test_patcher.py +0 -0
  170. {py2max-0.3.6 → py2max-0.4.1}/tests/test_pitched_osc.py +0 -0
  171. {py2max-0.3.6 → py2max-0.4.1}/tests/test_porttypes.py +0 -0
  172. {py2max-0.3.6 → py2max-0.4.1}/tests/test_presets.py +0 -0
  173. {py2max-0.3.6 → py2max-0.4.1}/tests/test_pydantic.py +0 -0
  174. {py2max-0.3.6 → py2max-0.4.1}/tests/test_rnbo.py +0 -0
  175. {py2max-0.3.6 → py2max-0.4.1}/tests/test_rnbo_subpatcher.py +0 -0
  176. {py2max-0.3.6 → py2max-0.4.1}/tests/test_scripting_name.py +0 -0
  177. {py2max-0.3.6 → py2max-0.4.1}/tests/test_search.py +0 -0
  178. {py2max-0.3.6 → py2max-0.4.1}/tests/test_semantic_ids.py +0 -0
  179. {py2max-0.3.6 → py2max-0.4.1}/tests/test_svg.py +0 -0
  180. {py2max-0.3.6 → py2max-0.4.1}/tests/test_svg_fidelity.py +0 -0
  181. {py2max-0.3.6 → py2max-0.4.1}/tests/test_table.py +0 -0
  182. {py2max-0.3.6 → py2max-0.4.1}/tests/test_transformers.py +0 -0
  183. {py2max-0.3.6 → py2max-0.4.1}/tests/test_transformers_e4.py +0 -0
  184. {py2max-0.3.6 → py2max-0.4.1}/tests/test_tree.py +0 -0
  185. {py2max-0.3.6 → py2max-0.4.1}/tests/test_tree_builder.py +0 -0
  186. {py2max-0.3.6 → py2max-0.4.1}/tests/test_tutorial_simple_synthesis.py +0 -0
  187. {py2max-0.3.6 → py2max-0.4.1}/tests/test_two_sines.py +0 -0
  188. {py2max-0.3.6 → py2max-0.4.1}/tests/test_umenu.py +0 -0
  189. {py2max-0.3.6 → py2max-0.4.1}/tests/test_utils.py +0 -0
  190. {py2max-0.3.6 → py2max-0.4.1}/tests/test_validate_attrs.py +0 -0
  191. {py2max-0.3.6 → py2max-0.4.1}/tests/test_validation.py +0 -0
  192. {py2max-0.3.6 → py2max-0.4.1}/tests/test_varname.py +0 -0
  193. {py2max-0.3.6 → py2max-0.4.1}/tests/test_wheel_bundle.py +0 -0
  194. {py2max-0.3.6 → py2max-0.4.1}/tests/test_zl_group.py +0 -0
@@ -2,12 +2,212 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.1]
6
+
7
+ Two rect-handling bugs, both of which broke documented entry points on any
8
+ patch that came off disk rather than out of a script.
9
+
10
+ ### Fixed
11
+
12
+ - **Layout operations work on patches loaded from disk.** JSON has no tuple type, so every rect in a loaded patch arrived as a plain list, while `Box.patching_rect` is declared `Optional[Rect]` and read attribute-wise (`rect.y`) by the layout managers. Any layout call on a loaded patch therefore died with `AttributeError: 'list' object has no attribute 'y'` -- including the documented `py2max optimize <file>` CLI subcommand, which failed on every input file. `Patcher.from_dict` and `Box.from_dict` now restore rect-valued keys (`rect`, `patching_rect`, `presentation_rect`) to `Rect`.
13
+
14
+ Round-trip fidelity is unaffected: `to_dict()` normalizes those keys back to plain lists, so the serialized form stays pure JSON and a load/save cycle still reproduces its input exactly. Only the three keys py2max actually interprets are touched; opaque passthrough props such as `client_rect`, `dstrect` and `storage_rect` are left alone.
15
+
16
+ - **`enforce_integer_coords()` no longer crashes on a `Rect`.** The rounding helper assigned to `rect.x`/`rect.y`/`rect.w`/`rect.h`, but `Rect` is a `NamedTuple` and immutable, so rounding any programmatically built patch with non-integer coordinates raised `AttributeError: can't set attribute`. A rounded rect is now rebuilt and assigned back, preserving the storage form (`Rect` in, `Rect` out; list in, list out).
17
+
18
+ ## [0.4.0]
19
+
20
+ Two things dominate this release.
21
+
22
+ **js2max**: a JavaScript counterpart to py2max that runs *inside* an open Max
23
+ patcher through the `v8` object, so a patch can build objects into itself and
24
+ serialize itself back out. Its runtime ships in the wheel, so
25
+ `p.add_v8_bridge()` and `p.save()` are all it takes. Confirmed end to end
26
+ against Max, including a file js2max wrote being opened by Max.
27
+
28
+ **Typed box properties**: the Max property vocabulary reaches the emitted patch
29
+ through `**kwds` no longer. `BoxProps` / `TextboxProps` mean a misspelled
30
+ `bgcolour` or a wrongly-typed `fontsize` is rejected by mypy with a suggestion,
31
+ where both previously shipped straight into the file.
32
+
33
+ The rest is nine fixes, several of them long-standing and silent -- comments
34
+ written with their port counts backwards since the beginning, `to_dict()`
35
+ returning an empty patcher depending on call order, and nulls reaching the patch
36
+ one level down.
37
+
38
+ ### New: js2max -- a JavaScript counterpart that builds patches inside Max
39
+
40
+ - **[`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.
41
+
42
+ - **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.
43
+
44
+ - **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.
45
+
46
+ - `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.
47
+
48
+ - 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.
49
+
50
+ ### New: the js2max runtime ships in the wheel; `add_v8_bridge()` writes it beside a patch
51
+
52
+ - 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.
53
+
54
+ - `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.
55
+
56
+ - **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.
57
+
58
+ - 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.
59
+
60
+ - 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.
61
+
62
+ - Documented in [the js2max guide](https://github.com/shakfu/py2max/blob/main/docs/user_guide/js2max.md).
63
+
64
+ ### New: box properties are typed -- a misspelled property is now an error, not a silent key
65
+
66
+ - 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.
67
+
68
+ - **`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.
69
+
70
+ - 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.
71
+
72
+ - **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.
73
+
74
+ - **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`).
75
+
76
+ - **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.
77
+
78
+ - 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.
79
+
80
+ - **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`.
81
+
82
+ - `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.
83
+
84
+ - `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.
85
+
86
+ - 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.
87
+
88
+ ### New: `scripts/py2max.py` is now generated, not hand-maintained
89
+
90
+ - `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.
91
+
92
+ - **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.
93
+
94
+ - `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.
95
+
96
+ - 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.
97
+
98
+ ### Fixed: `add_comment` had its port counts backwards
99
+
100
+ - 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.
101
+
102
+ - **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.
103
+
104
+ - 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.
105
+
106
+ ### Fixed: `to_dict()` returned an empty patcher until something else rendered
107
+
108
+ - `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.
109
+
110
+ - **`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.
111
+
112
+ - **`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.
113
+
114
+ - `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.
115
+
116
+ ### Fixed: box port counts -- explicit zeros, and subpatchers that track their contents
117
+
118
+ - `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.)
119
+
120
+ - **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.
121
+
122
+ - 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.
123
+
124
+ - `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.
125
+
126
+ - Regression tests in `tests/test_subpatch.py` cover all four cases: explicit zeros, port tracking, empty-subpatcher defaults, and `gen~`/`rnbo~` ports surviving.
127
+
128
+ ### Fixed: nulls no longer reach the patch -- `_remove_none_entries` recurses
129
+
130
+ - `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.
131
+
132
+ - 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.
133
+
134
+ - `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`.
135
+
136
+ ### Fixed: `add()` raised `TypeError` when a keyword named the target's own parameter
137
+
138
+ - `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.
139
+
140
+ - **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.
141
+
142
+ - **`.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.
143
+
144
+ - **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.
145
+
146
+ ### Fixed: `MaxRefDB.search()` treated `%` and `_` as wildcards
147
+
148
+ - 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 '\'`.
149
+
150
+ - **`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`.
151
+
152
+ ### Fixed: matrix layout fragmented a signal chain when boxes were added in reverse order
153
+
154
+ - `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.
155
+
156
+ - 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.
157
+
158
+ ### Fixed: importing py2max no longer logs, nor hijacks the host's logging
159
+
160
+ - **`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.
161
+
162
+ - **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.
163
+
164
+ - **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`.
165
+
166
+ - **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.
167
+
168
+ - **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.
169
+
170
+ - `config()` is retained as a no-op alias of `get_logger()` for backwards compatibility.
171
+
172
+ - `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.
173
+
174
+ ### Fixed: the maxref cache no longer prints to stderr
175
+
176
+ - 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.
177
+
178
+ ### Notes for upgraders
179
+
180
+ No API was removed and no call signature changed, but four things behave
181
+ differently enough to mention.
182
+
183
+ - **Every comment box changes shape.** `add_comment` wrote 0 inlets and 1 outlet;
184
+ a comment has 1 and 0. Regenerating a patch that contains comments produces a
185
+ different -- correct -- file, and Max was silently rewriting the values on save
186
+ anyway.
187
+ - **`to_dict()` renders**, so it returns the patcher as it stands rather than as
188
+ it stood after whatever last rendered it. It previously returned an empty
189
+ patcher until something else called `render()`. If you were calling
190
+ `render()` first, you no longer need to; if you were relying on the empty
191
+ result, you were relying on a bug.
192
+ - **`render()` is idempotent.** `reset_on_render=False` no longer accumulates
193
+ boxes across renders. It also never accumulated *lines*, so what it did before
194
+ was not coherent.
195
+ - **`save()` may now write a second file** -- but only for a patcher that called
196
+ `add_v8_bridge()`, which is new in this release. Nothing that worked before
197
+ writes anything extra.
198
+
199
+ Typed box properties are a static change: `mypy --strict` will now reject a
200
+ misspelled or wrongly-typed property that it previously accepted. That is the
201
+ point of them, and nothing changes at runtime.
202
+
5
203
  ## [0.3.6]
6
204
 
7
205
  ### Removed: incremental layout; `optimize_layout()` is batch-only again
8
206
 
9
207
  - `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).
208
+
10
209
  - **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.
210
+
11
211
  - **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
212
 
13
213
  ## [0.3.5]
@@ -25,12 +225,19 @@
25
225
  ### New: Patch linting and message-type-aware connection validation
26
226
 
27
227
  - 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.
228
+
28
229
  - **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`.
230
+
29
231
  - 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.
232
+
30
233
  - 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.
234
+
31
235
  - `py2max validate` (CLI) now reports the full lint result -- errors and warnings with codes -- and exits non-zero on any error.
236
+
32
237
  - A corpus test re-lints every shipped layout example patch and fails on any error, so Max-invalid wiring can no longer ship unnoticed.
238
+
33
239
  - 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`).
240
+
34
241
  - 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
242
 
36
243
  ## [0.3.3]
@@ -42,6 +249,7 @@
42
249
  ### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
43
250
 
44
251
  - `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.
252
+
45
253
  - 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
254
 
47
255
  ### Fixed: Clustered grid layout squashed object sizes
@@ -65,42 +273,55 @@
65
273
  ### New: Patch editing and removal API
66
274
 
67
275
  - 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.
276
+
68
277
  - 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
278
 
70
279
  ### New: Graph-layout engines as layout managers
71
280
 
72
281
  - 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`.
282
+
73
283
  - 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
284
 
75
285
  ### New: Layout gallery generator and docs page
76
286
 
77
287
  - `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.
288
+
78
289
  - 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.
290
+
79
291
  - 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
292
 
81
293
  ### Fixed: Generation correctness
82
294
 
83
295
  - `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).
296
+
84
297
  - `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
298
+
85
299
  - Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
300
+
86
301
  - Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
302
+
87
303
  - 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.
304
+
88
305
  - 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
306
 
90
307
  ### Fixed: Layout managers
91
308
 
92
309
  - `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
310
+
93
311
  - 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()`.
312
+
94
313
  - 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
314
 
96
315
  ### Improved: maxref bundle loading (lazy and thread-safe)
97
316
 
98
317
  - 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.
318
+
99
319
  - 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
320
 
101
321
  ### Fixed: maxref outlet digests and parser robustness
102
322
 
103
323
  - 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.
324
+
104
325
  - 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
326
 
106
327
  ### Improved: Cross-platform maxref discovery
@@ -110,20 +331,27 @@
110
331
  ### Improved: SVG preview fidelity
111
332
 
112
333
  - The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
334
+
113
335
  - **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.
336
+
114
337
  - **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.
338
+
115
339
  - **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).
340
+
116
341
  - `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
117
342
 
118
343
  ### Fixed: Layout classification and overlap resolution
119
344
 
120
345
  - 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.
346
+
121
347
  - `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
348
 
123
349
  ### Improved: Patch transformers
124
350
 
125
351
  - `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
352
+
126
353
  - 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.
354
+
127
355
  - 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
356
 
129
357
  ### Fixed: Converters module cleanup
@@ -137,12 +365,15 @@
137
365
  ### Removed: MaxRefDB deprecated alias methods
138
366
 
139
367
  - 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.
368
+
140
369
  - 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
370
 
142
371
  ### Removed: Obsolete layout experiments and vendored editor assets
143
372
 
144
373
  - 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`.
374
+
145
375
  - 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.
376
+
146
377
  - 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
378
 
148
379
  ## [0.3.1]
@@ -150,8 +381,11 @@
150
381
  ### New: Standalone `gen.codebox~` Support
151
382
 
152
383
  - `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.
384
+
153
385
  - 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.
386
+
154
387
  - 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.
388
+
155
389
  - 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
390
 
157
391
  ## [0.3.0]
@@ -161,7 +395,9 @@
161
395
  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
396
 
163
397
  - Removed `Patcher.serve()` and the `py2max serve` / `py2max repl` CLI commands; those CLI subcommands now print a pointer to `py2max-server`.
398
+
164
399
  - Removed the `[server]` optional-dependency extra (`websockets`, `ptpython`) and the bundled browser assets (`py2max/static/`).
400
+
165
401
  - 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
402
 
167
403
  ### New: `Patcher.encapsulate()`
@@ -171,6 +407,7 @@ The browser-based live editor and remote REPL have moved to a separate companion
171
407
  ### New: Preset / `pattrstorage` Scaffolding
172
408
 
173
409
  - `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.
410
+
174
411
  - `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
412
 
176
413
  ### New: Keyword-Attribute Validation (`validate_attrs`)
@@ -180,6 +417,7 @@ The browser-based live editor and remote REPL have moved to a separate companion
180
417
  ### New: Multichannel (`mc.`) / Polyphony Helpers
181
418
 
182
419
  - `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`).
420
+
183
421
  - `Patcher.add_poly(target, voices=1)` adds a `poly~` object hosting N voices of a target patch.
184
422
 
185
423
  ### Improved: SVG Export (Max-faithful preview)
@@ -193,7 +431,9 @@ The browser-based live editor and remote REPL have moved to a separate companion
193
431
  ### New: Color / Theme Helpers
194
432
 
195
433
  - `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.
434
+
196
435
  - `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.
436
+
197
437
  - `py2max.core.colors` exposes the `MAX_COLORS` named palette and `resolve_color()`.
198
438
 
199
439
  ### Security
@@ -215,14 +455,19 @@ The browser-based live editor and remote REPL have moved to a separate companion
215
455
  ### Fixed
216
456
 
217
457
  - 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.
458
+
218
459
  - `Box.oid` now returns the trailing numeric part of any id (e.g. `cycle_1` -> 1) instead of raising `ValueError` under `semantic_ids=True`.
460
+
219
461
  - The `py2max` CLI now reports all `Py2MaxError`s (not just `InvalidConnectionError`) as a clean error message instead of leaking a traceback.
462
+
220
463
  - Fixed an `inital` -> `initial` keyword typo in the simple-synthesis tutorial.
221
464
 
222
465
  ### Testing & Tooling
223
466
 
224
467
  - 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.
468
+
225
469
  - 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.
470
+
226
471
  - Repo-wide `ruff` lint and format cleanup.
227
472
 
228
473
  ### New: Max for Live Support (`py2max.m4l`)
@@ -230,20 +475,27 @@ The browser-based live editor and remote REPL have moved to a separate companion
230
475
  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
476
 
232
477
  - **`.amxd` read/write**: byte-for-byte compatible with Max-exported devices; verified against real fixtures and end-to-end in Live 12.
478
+
233
479
  - **Device-type discrimination**: Audio Effect / Instrument / MIDI Effect via `Patcher(device_type=...)` or the `pack_amxd` / `write_amxd` `device_type` argument.
480
+
234
481
  - **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).
482
+
235
483
  - `Patcher.save()` / `Patcher.from_file()` auto-detect the `.amxd` extension; `.maxpat` path is unchanged.
236
484
 
237
485
  ### Changed: M4L Module Layout & Imports
238
486
 
239
487
  - All M4L code (binary format + presentation helpers) lives in a single module `py2max/m4l.py`. Previously briefly split as `py2max/amxd.py`.
488
+
240
489
  - M4L symbols are reachable only via `from py2max.m4l import …`; nothing is re-exported from the top-level `py2max` namespace.
241
490
 
242
491
  ### New: Prebuilt MaxRef Bundle (Linux Support)
243
492
 
244
493
  - Ship `py2max/maxref/data/bundle.json.gz` in the wheel (1175 objects, ~1 MiB compressed, ~7 MiB raw).
494
+
245
495
  - `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.
496
+
246
497
  - Regenerate with `uv run python scripts/build_maxref_bundle.py` on a machine with Max installed; commit the result.
498
+
247
499
  - Bundle stores full parsed data (methods, attributes, inlets/outlets, digests, descriptions) — not a trimmed subset — so introspection parity with macOS/Windows is preserved.
248
500
 
249
501
  ## [0.2.1] - 2026-01-11
@@ -251,55 +503,81 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
251
503
  ### New: Dagre Layout Algorithm
252
504
 
253
505
  - Added Dagre (Directed Acyclic Graph) as third layout algorithm option alongside WebCola and ELK
506
+
254
507
  - Integrated `dagre-bundle.js` combining graphlib with require shim for browser compatibility
508
+
255
509
  - Added Dagre-specific controls: Ranker (network-simplex, longest-path, tight-tree) and Align options
510
+
256
511
  - Supports all flow directions: top-bottom, bottom-top, left-right, right-left
257
512
 
258
513
  ### Improved: Interactive Editor Visualization
259
514
 
260
515
  - **ViewBox Scaling**: Dynamic padding (10% of content, min 30px, max 100px) with aspect ratio preservation
516
+
261
517
  - **Port Position Safety**: Added bounds checking with `safeIndex` clamping to prevent invalid port positions
518
+
262
519
  - **Patchline Animation**: Added `animatePatchlines()` method for smooth patchline transitions during layout
520
+
263
521
  - **Layout Centering**: Added `centerLayout()` helper method - all three algorithms now center content within canvas
522
+
264
523
  - **Delta Updates**: Position updates now send only changed box data instead of full patcher state
524
+
265
525
  - Added `updateBoxPosition()` for efficient single-box DOM updates
526
+
266
527
  - Added `updateConnectedLines()` to update patchlines without full re-render
528
+
267
529
  - Significantly reduces bandwidth during drag operations
268
530
 
269
531
  ### Improved: FlowLayoutManager
270
532
 
271
533
  - **Line Crossing Minimization**: Added `_minimize_crossings()` method using barycenter heuristic
534
+
272
535
  - Objects within each level are reordered based on average position of connected objects in previous level
536
+
273
537
  - Reduces visual line crossings for cleaner layouts
538
+
274
539
  - **Negative Position Prevention**: Added bounds clamping and auto-scaling when content exceeds available space
540
+
275
541
  - **Incremental Layout**: Supports `optimize_layout(changed_objects)` for efficient partial updates
276
542
 
277
543
  ### Improved: GridLayoutManager
278
544
 
279
545
  - Fixed integer division to float division for consistent cluster positioning
546
+
280
547
  - Now uses consistent float spacing within clusters
548
+
281
549
  - **Incremental Layout**: Supports `optimize_layout(changed_objects)` for efficient partial updates
282
550
 
283
551
  ### Improved: WebSocket Server Security
284
552
 
285
553
  - **Input Validation**: Added comprehensive schema-based message validation
554
+
286
555
  - `MESSAGE_SCHEMAS` defines required fields and types for each message type
556
+
287
557
  - `MAX_STRING_LENGTHS` prevents abuse (256 chars for IDs, 10000 for text, 4096 for filepaths)
558
+
288
559
  - `COORDINATE_BOUNDS` validates positions (-100000 to 100000)
560
+
289
561
  - Checks for control characters in strings
562
+
290
563
  - Validates optional fields (outlet/inlet indices 0-255)
564
+
291
565
  - Validation errors sent back to client as error messages
292
566
 
293
567
  ### New: Save As Dialog
294
568
 
295
569
  - Added `save_as_required` message type when patcher has no filepath
570
+
296
571
  - Added `handle_save_as()` handler for saving with specified filepath
572
+
297
573
  - Added `showSaveAsDialog()` in JavaScript with filename prompt
574
+
298
575
  - Automatically adds `.maxpat` extension if not provided
299
576
 
300
577
  ### Fixed: ELK Layout
301
578
 
302
579
  - Fixed "Referenced shape does not exist" errors by validating edges before creating ports
580
+
303
581
  - Ports now created based on actual connections, not just declared counts
304
582
 
305
583
  ### Fixed: Static File Paths
@@ -309,12 +587,19 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
309
587
  ### Improved: Base LayoutManager
310
588
 
311
589
  - Added `prevent_overlaps()` method for iterative overlap prevention
590
+
312
591
  - **Incremental Layout System**: Added smart layout optimization that only repositions affected objects
592
+
313
593
  - `optimize_layout(changed_objects)` accepts optional set of changed object IDs
594
+
314
595
  - `should_use_incremental()` determines when to use incremental vs full layout (30% threshold)
596
+
315
597
  - `get_affected_objects()` finds changed objects plus their connected neighbors
598
+
316
599
  - `_incremental_layout()` repositions only affected objects using spiral search
600
+
317
601
  - `_find_non_overlapping_position()` finds nearby positions that don't overlap with fixed objects
602
+
318
603
  - `_full_layout()` for complete layout recalculation (subclasses override)
319
604
 
320
605
  ## [0.2.0]
@@ -322,21 +607,29 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
322
607
  ### Updated: Optional Layout Dependencies
323
608
 
324
609
  - Updated `pycola` dependency to `graph-layout` package (<https://github.com/shakfu/graph-layout>)
610
+
325
611
  - Renamed test file from `test_layout_pycola.py` to `test_layout_graph_layout.py`
612
+
326
613
  - Updated API to use `ColaLayoutAdapter` from `graph_layout` module
327
614
 
328
615
  - Updated `pyhola` dependency to `hola-graph` package (<https://github.com/shakfu/hola-graph>)
616
+
329
617
  - Renamed test file from `test_layout_pyhola.py` to `test_layout_hola_graph.py`
618
+
330
619
  - Updated imports to use `hola_graph._core` module
331
620
 
332
621
  - Fixed `test_layout_networkx2.py` to properly check for `pygraphviz` dependency
622
+
333
623
  - Test now correctly skips when pygraphviz is not installed
334
624
 
335
625
  ### Simplified: Optional Dependencies
336
626
 
337
627
  - Consolidated optional dependencies in `pyproject.toml` to single `server` option
628
+
338
629
  - Removed `repl` and `all` options
630
+
339
631
  - `server` now includes both `websockets` and `ptpython`
632
+
340
633
  - Install with: `pip install py2max[server]`
341
634
 
342
635
  ### New: Interactive Editor - Advanced Layout with SVG.js, WebCola, and D3.js
@@ -348,16 +641,25 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
348
641
  - Added interactive auto-layout controls panel with real-time parameter adjustment
349
642
 
350
643
  - Added 5 adjustable layout parameters via sliders and controls:
644
+
351
645
  - **Link Distance** (50-300): Controls spacing between connected objects
646
+
352
647
  - **Iterations** (10-200): Controls layout quality and convergence
648
+
353
649
  - **Canvas Width** (400-1600): Adjustable layout area width
650
+
354
651
  - **Canvas Height** (300-1200): Adjustable layout area height
652
+
355
653
  - **Avoid Overlaps** (checkbox): Toggle automatic overlap prevention
356
654
 
357
655
  - Added constraint-based layout system with 4 presets:
656
+
358
657
  - **None**: Natural force-directed layout without alignment constraints
658
+
359
659
  - **Horizontal Flow**: Aligns objects in horizontal rows (left-to-right signal flow)
660
+
360
661
  - **Vertical Flow**: Aligns objects in vertical columns (top-to-bottom signal flow)
662
+
361
663
  - **Grid**: Strict grid alignment with both row and column constraints
362
664
 
363
665
  - Added smooth SVG.js animations (500ms ease-in-out) for layout transitions
@@ -371,50 +673,75 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
371
673
  **SVG.js Implementation:**
372
674
 
373
675
  - Refactored all SVG rendering to use SVG.js declarative API instead of native DOM manipulation
676
+
374
677
  - `initializeSVG()`: Creates SVG canvas and layer groups using SVG.js
678
+
375
679
  - `createBox()`: Renders boxes with rectangles, text, and clipping paths using SVG.js
680
+
376
681
  - `createLine()`: Renders connection lines with hitboxes using SVG.js
682
+
377
683
  - `addPorts()`: Renders inlet/outlet circles using SVG.js
684
+
378
685
  - `autoLayout()`: Animates box movements using SVG.js transforms
379
686
 
380
687
  **WebCola Integration:**
381
688
 
382
689
  - Force-directed graph layout with configurable parameters
690
+
383
691
  - Constraint-based positioning using alignment constraints
692
+
384
693
  - Automatic overlap avoidance with adjustable node dimensions
694
+
385
695
  - Handles disconnected graph components gracefully
696
+
386
697
  - Jaccard link lengths for natural connection spacing
387
698
 
388
699
  **Constraint System:**
389
700
 
390
701
  - Automatic constraint generation based on object proximity (50px threshold)
702
+
391
703
  - Alignment constraints for horizontal rows (Y-axis alignment)
704
+
392
705
  - Alignment constraints for vertical columns (X-axis alignment)
706
+
393
707
  - Grid constraints combining both row and column alignment
708
+
394
709
  - Real-time constraint application with visual feedback
395
710
 
396
711
  **Documentation:**
397
712
 
398
713
  - Added comprehensive `docs/LIBRARIES_INTEGRATION.md` (518 lines)
714
+
399
715
  - Detailed parameter descriptions and effects
716
+
400
717
  - Constraint preset usage examples
718
+
401
719
  - Testing procedures and expected behavior
720
+
402
721
  - Code examples and API documentation
722
+
403
723
  - Performance considerations for different patch sizes
404
724
 
405
725
  **Demo Scripts:**
406
726
 
407
727
  - Added `examples/auto_layout_demo.py`: Complex synthesizer with randomized positions (13 objects, 16 connections)
728
+
408
729
  - Hierarchical layout demo: Tree structure with multiple processing layers (12 objects)
409
730
 
410
731
  **Benefits:**
411
732
 
412
733
  - Professional animated transitions for all layout operations
734
+
413
735
  - Interactive experimentation with layout parameters
736
+
414
737
  - Structured layouts matching typical Max patch patterns
738
+
415
739
  - Clean, maintainable SVG.js codebase
740
+
416
741
  - Four layout presets for different use cases
742
+
417
743
  - Real-time visual feedback
744
+
418
745
  - Minimal overhead (234KB total: D3 + SVG.js + WebCola, minified)
419
746
 
420
747
  **Example Usage:**
@@ -435,44 +762,63 @@ py2max serve outputs/auto_layout_demo.maxpat
435
762
  ### New: Interactive Editor - Nested Patcher Navigation
436
763
 
437
764
  - Added full nested patcher (subpatcher) navigation support in interactive editor
765
+
438
766
  - Double-click on subpatcher boxes (blue dashed border) to navigate into them
767
+
439
768
  - Navigate back using "Parent" button or ESC key
769
+
440
770
  - Breadcrumb navigation displays current location (e.g., "Main / Oscillator / Envelope")
771
+
441
772
  - Subpatcher boxes are fully interactive: draggable, connectable, deletable
773
+
442
774
  - Visual distinction: subpatcher boxes have blue dashed borders and bold blue text
775
+
443
776
  - Event delegation for reliable double-click detection even with dynamic DOM updates
777
+
444
778
  - Automatic parent reference restoration when loading patches from files
445
779
 
446
780
  **Server-Side Changes:**
447
781
 
448
782
  - Modified `get_patcher_state_json()` to include `has_subpatcher` flag and `patcher_path` breadcrumb
783
+
449
784
  - Added `handle_navigate_to_subpatcher()`, `handle_navigate_to_parent()`, `handle_navigate_to_root()` handlers
785
+
450
786
  - Fixed inlet/outlet count detection to use `numinlets`/`numoutlets` attributes from loaded files
787
+
451
788
  - Handler now tracks both `root_patcher` (for saving) and `patcher` (current view)
452
789
 
453
790
  **Client-Side Changes:**
454
791
 
455
792
  - Added breadcrumb UI showing patcher hierarchy
793
+
456
794
  - Implemented event delegation for double-click handling on dynamically created boxes
795
+
457
796
  - Fixed object positioning by flattening `patching_rect` into `x`, `y`, `width`, `height`
797
+
458
798
  - CSS styling for subpatcher boxes with distinct visual appearance
799
+
459
800
  - ESC key navigation support
460
801
 
461
802
  **Core Changes:**
462
803
 
463
804
  - Modified `Patcher.from_dict()` to set `_parent` references for nested subpatchers when loading from files
805
+
464
806
  - Ensures bidirectional parent-child relationships for proper navigation
465
807
 
466
808
  **Tests:**
467
809
 
468
810
  - Added 14 comprehensive tests in `tests/test_nested_patchers.py`
811
+
469
812
  - All tests passing (326 passed, 14 skipped)
470
813
 
471
814
  **Demo:**
472
815
 
473
816
  - Added `examples/nested_patcher_demo.py` with three demonstration patches:
817
+
474
818
  - Synthesizer with nested envelope subpatcher
819
+
475
820
  - Effects chain with parallel subpatchers
821
+
476
822
  - Deeply nested hierarchy (6 levels)
477
823
 
478
824
  ### New: SVG Preview Feature
@@ -484,8 +830,11 @@ py2max serve outputs/auto_layout_demo.maxpat
484
830
  - Added `export_svg()` and `export_svg_string()` functions for programmatic SVG generation
485
831
 
486
832
  - Added SVG rendering for boxes with type-specific styling:
833
+
487
834
  - Regular objects: Light gray fill
835
+
488
836
  - Comments: Yellow fill (#ffffd0)
837
+
489
838
  - Messages: Medium gray fill
490
839
 
491
840
  - Added patchline rendering with correct inlet/outlet connection points
@@ -550,10 +899,15 @@ svg_content = export_svg_string(p, show_ports=True)
550
899
  **Benefits:**
551
900
 
552
901
  - No Max installation required for visual validation
902
+
553
903
  - High-quality, scalable vector graphics
904
+
554
905
  - Works with all py2max layout managers
906
+
555
907
  - Perfect for CI/CD, documentation, and version control
908
+
556
909
  - Pure Python implementation with no binary dependencies
910
+
557
911
  - Viewable in any web browser
558
912
 
559
913
  ### New: SQLite Database Support
@@ -593,24 +947,39 @@ svg_content = export_svg_string(p, show_ports=True)
593
947
  **Python API Improvements:**
594
948
 
595
949
  - Added Pythonic properties: `.count`, `.categories`, `.objects` for cleaner access
950
+
596
951
  - Added magic methods: `len(db)`, `'obj' in db`, `db['obj']`, `repr(db)` for natural Python usage
952
+
597
953
  - Added simplified methods: `populate()`, `search()`, `by_category()`, `export()`, `load()` with cleaner naming
954
+
598
955
  - Added `summary()` method for database statistics with category breakdown
956
+
599
957
  - Maintained full backward compatibility with deprecated methods
958
+
600
959
  - All 18 database tests pass
601
960
 
602
961
  **CLI Improvements:**
603
962
 
604
963
  - Added comprehensive `py2max db` subcommand with 7 operations:
964
+
605
965
  - `db create` - Create new databases with optional category filtering
966
+
606
967
  - `db populate` - Add objects to existing databases
968
+
607
969
  - `db info` - Show database information with summary and listing options
970
+
608
971
  - `db search` - Search objects by text or category with verbose mode
972
+
609
973
  - `db query` - Get detailed object information (JSON, dict, or human-readable)
974
+
610
975
  - `db export` - Export database to JSON
976
+
611
977
  - `db import` - Import JSON data into database
978
+
612
979
  - Updated `convert maxref-to-sqlite` to use MaxRefDB internally
980
+
613
981
  - Added 7 new CLI tests covering all db subcommands
982
+
614
983
  - All 272 tests pass (258 passed, 14 skipped)
615
984
 
616
985
  **Example Usage:**
@@ -647,31 +1016,41 @@ py2max db cache clear
647
1016
  MaxRefDB now automatically creates and populates a cache database on first use:
648
1017
 
649
1018
  - **macOS**: `~/Library/Caches/py2max/maxref.db`
1019
+
650
1020
  - **Linux**: `~/.cache/py2max/maxref.db`
1021
+
651
1022
  - **Windows**: `~/AppData/Local/py2max/Cache/maxref.db`
652
1023
 
653
1024
  **Benefits:**
654
1025
 
655
1026
  - One-time population of all 1157 Max objects
1027
+
656
1028
  - Instant access on subsequent use
1029
+
657
1030
  - No manual setup required
1031
+
658
1032
  - Platform-appropriate cache location
659
1033
 
660
1034
  **New Static Methods:**
661
1035
 
662
1036
  - `MaxRefDB.get_cache_dir()` - Get platform-specific cache directory
1037
+
663
1038
  - `MaxRefDB.get_default_db_path()` - Get default database path
664
1039
 
665
1040
  **Updated API:**
666
1041
 
667
1042
  - `MaxRefDB()` - Now uses cache by default
1043
+
668
1044
  - `MaxRefDB(db_path, auto_populate=True)` - Control auto-population
1045
+
669
1046
  - `MaxRefDB(':memory:')` - In-memory database (no caching)
670
1047
 
671
1048
  **New CLI Commands:**
672
1049
 
673
1050
  - `py2max db cache location` - Show cache location and status
1051
+
674
1052
  - `py2max db cache init` - Manually initialize cache
1053
+
675
1054
  - `py2max db cache clear` - Clear cache database
676
1055
 
677
1056
  **Example Usage:**