py2max 0.3.0__tar.gz → 0.3.3__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.
- {py2max-0.3.0 → py2max-0.3.3}/CHANGELOG.md +121 -0
- {py2max-0.3.0 → py2max-0.3.3}/PKG-INFO +67 -5
- {py2max-0.3.0 → py2max-0.3.3}/README.md +62 -4
- {py2max-0.3.0 → py2max-0.3.3}/py2max/__init__.py +1 -1
- {py2max-0.3.0 → py2max-0.3.3}/py2max/cli.py +9 -69
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/abstract.py +2 -2
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/factory.py +281 -22
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/patcher.py +69 -5
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/serialization.py +3 -6
- {py2max-0.3.0 → py2max-0.3.3}/py2max/export/converters.py +21 -38
- py2max-0.3.3/py2max/export/svg.py +565 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/__init__.py +4 -1
- {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/base.py +68 -92
- py2max-0.3.3/py2max/layout/external.py +238 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/flow.py +12 -24
- py2max-0.3.3/py2max/layout/graph.py +173 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/grid.py +9 -53
- {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/matrix.py +129 -146
- {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/category.py +5 -4
- py2max-0.3.3/py2max/maxref/data/bundle.json.gz +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/db.py +2 -88
- {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/legacy.py +7 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/parser.py +136 -54
- {py2max-0.3.0 → py2max-0.3.3}/py2max/transformers.py +69 -6
- {py2max-0.3.0 → py2max-0.3.3}/py2max/utils.py +17 -1
- {py2max-0.3.0 → py2max-0.3.3}/pyproject.toml +26 -1
- py2max-0.3.3/tests/conftest.py +98 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/auto_layout_demo.py +1 -1
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/db/category_db_demo.py +16 -16
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/db/maxref_db_demo.py +13 -13
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/columnar_layout_examples.py +24 -27
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/matrix_layout_examples.py +11 -14
- py2max-0.3.3/tests/test_abstraction.py +34 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_amxd.py +3 -1
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_attrui.py +37 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_basic.py +4 -3
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_beap.py +8 -1
- py2max-0.3.3/tests/test_bpatcher.py +44 -0
- py2max-0.3.3/tests/test_coll.py +28 -0
- py2max-0.3.3/tests/test_colors.py +44 -0
- py2max-0.3.3/tests/test_comment.py +20 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_db.py +15 -15
- py2max-0.3.3/tests/test_defaults.py +26 -0
- py2max-0.3.3/tests/test_dict.py +15 -0
- py2max-0.3.3/tests/test_edit_operations.py +362 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_examples.py +27 -0
- py2max-0.3.3/tests/test_ezdac.py +21 -0
- py2max-0.3.3/tests/test_gen.py +96 -0
- py2max-0.3.3/tests/test_itable.py +15 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_js.py +14 -2
- py2max-0.3.3/tests/test_kwds_filter.py +49 -0
- py2max-0.3.3/tests/test_layout_box_dims.py +81 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_builtins.py +1 -1
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_coverage.py +33 -23
- py2max-0.3.3/tests/test_layout_graph.py +77 -0
- py2max-0.3.3/tests/test_layout_graph_layout.py +103 -0
- py2max-0.3.3/tests/test_layout_graph_manager.py +127 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_matrix.py +145 -41
- py2max-0.3.3/tests/test_layout_overlaps.py +66 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_vertical.py +21 -0
- py2max-0.3.3/tests/test_linking.py +76 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_m4l.py +0 -6
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_maxref_bundle.py +8 -7
- py2max-0.3.3/tests/test_maxref_lazy.py +55 -0
- py2max-0.3.3/tests/test_maxref_parser.py +60 -0
- py2max-0.3.3/tests/test_maxref_refpages.py +85 -0
- py2max-0.3.3/tests/test_mc_cycle.py +19 -0
- py2max-0.3.3/tests/test_message.py +16 -0
- py2max-0.3.3/tests/test_mypatch.py +22 -0
- py2max-0.3.3/tests/test_number_tilde.py +87 -0
- py2max-0.3.3/tests/test_numbers.py +56 -0
- py2max-0.3.3/tests/test_param.py +36 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_pitched_osc.py +12 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_pydantic.py +29 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_rnbo.py +78 -1
- py2max-0.3.3/tests/test_rnbo_subpatcher.py +36 -0
- py2max-0.3.3/tests/test_scripting_name.py +18 -0
- py2max-0.3.3/tests/test_subpatch.py +34 -0
- py2max-0.3.3/tests/test_svg_fidelity.py +63 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_table.py +32 -2
- py2max-0.3.3/tests/test_transformers_e4.py +68 -0
- py2max-0.3.3/tests/test_tutorial_simple_synthesis.py +100 -0
- py2max-0.3.3/tests/test_umenu.py +18 -0
- py2max-0.3.3/tests/test_varname.py +26 -0
- py2max-0.3.3/tests/test_wheel_bundle.py +76 -0
- py2max-0.3.3/tests/test_zl_group.py +28 -0
- py2max-0.3.0/py2max/export/svg.py +0 -456
- py2max-0.3.0/py2max/maxref/data/bundle.json.gz +0 -0
- py2max-0.3.0/tests/conftest.py +0 -21
- py2max-0.3.0/tests/registry.py +0 -1526
- py2max-0.3.0/tests/scratch.py +0 -80
- py2max-0.3.0/tests/test_abstraction.py +0 -21
- py2max-0.3.0/tests/test_bpatcher.py +0 -20
- py2max-0.3.0/tests/test_coll.py +0 -16
- py2max-0.3.0/tests/test_colors.py +0 -9
- py2max-0.3.0/tests/test_comment.py +0 -8
- py2max-0.3.0/tests/test_defaults.py +0 -11
- py2max-0.3.0/tests/test_dict.py +0 -7
- py2max-0.3.0/tests/test_ezdac.py +0 -10
- py2max-0.3.0/tests/test_gen.py +0 -27
- py2max-0.3.0/tests/test_itable.py +0 -7
- py2max-0.3.0/tests/test_kwds_filter.py +0 -116
- py2max-0.3.0/tests/test_layout_graph_layout.py +0 -113
- py2max-0.3.0/tests/test_layout_hola1.py +0 -117
- py2max-0.3.0/tests/test_layout_hola2.py +0 -100
- py2max-0.3.0/tests/test_layout_hola3.py +0 -123
- py2max-0.3.0/tests/test_layout_hola_graph.py +0 -124
- py2max-0.3.0/tests/test_layout_networkx1.py +0 -128
- py2max-0.3.0/tests/test_layout_networkx2.py +0 -96
- py2max-0.3.0/tests/test_layout_nx_graphviz.py +0 -96
- py2max-0.3.0/tests/test_layout_nx_orthogonal.py +0 -120
- py2max-0.3.0/tests/test_layout_nx_tsmpy.py +0 -108
- py2max-0.3.0/tests/test_linking.py +0 -40
- py2max-0.3.0/tests/test_mc_cycle.py +0 -7
- py2max-0.3.0/tests/test_message.py +0 -7
- py2max-0.3.0/tests/test_mypatch.py +0 -12
- py2max-0.3.0/tests/test_number_tilde.py +0 -52
- py2max-0.3.0/tests/test_numbers.py +0 -34
- py2max-0.3.0/tests/test_param.py +0 -8
- py2max-0.3.0/tests/test_rnbo_subpatcher.py +0 -19
- py2max-0.3.0/tests/test_scripting_name.py +0 -7
- py2max-0.3.0/tests/test_subpatch.py +0 -17
- py2max-0.3.0/tests/test_tutorial_simple_synthesis.py +0 -44
- py2max-0.3.0/tests/test_umenu.py +0 -11
- py2max-0.3.0/tests/test_varname.py +0 -11
- py2max-0.3.0/tests/test_zl_group.py +0 -11
- {py2max-0.3.0 → py2max-0.3.3}/LICENSE +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/__main__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/box.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/colors.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/common.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/core/patchline.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/exceptions.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/export/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/log.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/m4l.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/py2max/py.typed +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/__init__.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/complex.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/desc.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/empty.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/mydevice.amxd +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/mydevice2.amxd +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/nested.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/simple.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/tabular.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/data/umenu.maxref.xml +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/README.md +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/connection_patterns.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/custom_extensions.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/data_containers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/error_handling.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/performance_optimization.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/subpatchers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/api/box_api_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/api/patcher_api_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/flow_layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/grid_layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/basic_synth.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/basic_synth.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/complex_synth.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/complex_synth.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/flow_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/flow_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/grid_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/grid_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/horizontal_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/horizontal_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_no_ports.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_no_title.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_patch.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_with_ports.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/svg_preview_demo.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/vertical_layout.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/vertical_layout.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/workflow_demo.maxpat +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/workflow_demo.svg +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/quickstart/basic_patch.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/quickstart/layout_examples.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/generative_music.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/interactive_controller.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/signal_processing_chain.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/simple_synthesis.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/graphs/random/v30e33.tglf +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_abstract_coverage.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_add.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_cli.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_colors_theme.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_connection_validation.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_converters.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_core_coverage.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_encapsulate.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_error_handling.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_group.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_flow.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_maxref.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_mc_poly.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_nested.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_nested_patchers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_patcher.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_presets.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_search.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_semantic_ids.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_svg.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_transformers.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_tree.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_tree_builder.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_two_sines.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_utils.py +0 -0
- {py2max-0.3.0 → py2max-0.3.3}/tests/test_validate_attrs.py +0 -0
|
@@ -2,6 +2,127 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.3]
|
|
6
|
+
|
|
7
|
+
### Removed: `serve` and `repl` CLI subcommands
|
|
8
|
+
|
|
9
|
+
- The `py2max serve` and `py2max repl` subcommands -- thin stubs that only pointed at the separate `py2max-server` package since v0.3.0 -- have been removed. The interactive live editor and remote REPL still live in `py2max-server` (`pip install py2max-server`).
|
|
10
|
+
|
|
11
|
+
### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
|
|
12
|
+
|
|
13
|
+
- `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.
|
|
14
|
+
- 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).
|
|
15
|
+
|
|
16
|
+
### Fixed: Clustered grid layout squashed object sizes
|
|
17
|
+
|
|
18
|
+
- The connection-aware clustering path of `GridLayoutManager` (`cluster_connected=True`) still wrote the manager's uniform 66x22 size back onto every clustered object, squashing UI objects (`scope~`, `ezdac~`, `dial`, ...) to text-box size. It now preserves each object's real width/height, matching the already-fixed non-clustered path.
|
|
19
|
+
|
|
20
|
+
### Fixed: Example patches use valid Max connections
|
|
21
|
+
|
|
22
|
+
- The layout example scripts (`tests/examples/layout/`) and matrix-layout tests wired control objects (`metro`, `loadbang`) straight into oscillator signal inlets, which Max rejects ("error connecting outlet ... to ... inlet"). They now use the correct idiom -- a float number box sets the oscillator frequency, `metro` triggers envelopes, and envelopes modulate amplitude through a `*~` VCA -- so the generated demo patches load cleanly.
|
|
23
|
+
|
|
24
|
+
### New: `kwds_filter` utility
|
|
25
|
+
|
|
26
|
+
- Added `py2max.utils.kwds_filter(kwds, **elems)`: returns `kwds` merged with the `elems` whose value is not `None` (legitimate falsy values such as `0` / `""` are kept, and the input is not mutated). Lets a method keep an optional parameter in its signature but omit it from the forwarded `**kwds` when the caller leaves it unset.
|
|
27
|
+
|
|
28
|
+
### Improved: Build and test-output hygiene
|
|
29
|
+
|
|
30
|
+
- Coverage HTML now writes to `build/coverage-html` and the test suite writes its artifacts under `build/test-output/` -- both within the git-ignored `build/` tree -- instead of a tracked `outputs/` directory. A new `make test-outputs` target writes all test artifacts flat into `build/test-outputs/` for quick inspection.
|
|
31
|
+
|
|
32
|
+
## [0.3.2]
|
|
33
|
+
|
|
34
|
+
### New: Patch editing and removal API
|
|
35
|
+
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
|
|
39
|
+
### New: Graph-layout engines as layout managers
|
|
40
|
+
|
|
41
|
+
- 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`.
|
|
42
|
+
- 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`.
|
|
43
|
+
|
|
44
|
+
### New: Layout gallery generator and docs page
|
|
45
|
+
|
|
46
|
+
- `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.
|
|
47
|
+
- 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.
|
|
48
|
+
- 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.
|
|
49
|
+
|
|
50
|
+
### Fixed: Generation correctness
|
|
51
|
+
|
|
52
|
+
- `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).
|
|
53
|
+
- `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
|
|
54
|
+
- Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
|
|
55
|
+
- Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
|
|
56
|
+
- 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.
|
|
57
|
+
- 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.
|
|
58
|
+
|
|
59
|
+
### Fixed: Layout managers
|
|
60
|
+
|
|
61
|
+
- `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
|
|
62
|
+
- 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()`.
|
|
63
|
+
- 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).
|
|
64
|
+
|
|
65
|
+
### Improved: maxref bundle loading (lazy and thread-safe)
|
|
66
|
+
|
|
67
|
+
- 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.
|
|
68
|
+
- 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.
|
|
69
|
+
|
|
70
|
+
### Fixed: maxref outlet digests and parser robustness
|
|
71
|
+
|
|
72
|
+
- 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.
|
|
73
|
+
- 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 `&`, `"`, `µ` are left intact).
|
|
74
|
+
|
|
75
|
+
### Improved: Cross-platform maxref discovery
|
|
76
|
+
|
|
77
|
+
- Reference-page resolution now honors the `PY2MAX_MAX_REFPAGES` environment override on all platforms and auto-discovers a Windows `refpages` directory, so Windows users with Max installed read live reference data instead of always falling back to the bundled snapshot. A wheel-build test now asserts the offline `bundle.json.gz` actually ships in the built artifact.
|
|
78
|
+
|
|
79
|
+
### Improved: SVG preview fidelity
|
|
80
|
+
|
|
81
|
+
- The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
|
|
82
|
+
- **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.
|
|
83
|
+
- **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.
|
|
84
|
+
- **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).
|
|
85
|
+
- `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
|
|
86
|
+
|
|
87
|
+
### Fixed: Layout classification and overlap resolution
|
|
88
|
+
|
|
89
|
+
- 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.
|
|
90
|
+
- `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.
|
|
91
|
+
|
|
92
|
+
### Improved: Patch transformers
|
|
93
|
+
|
|
94
|
+
- `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
|
|
95
|
+
- 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.
|
|
96
|
+
- 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).
|
|
97
|
+
|
|
98
|
+
### Fixed: Converters module cleanup
|
|
99
|
+
|
|
100
|
+
- Importing `py2max.export.converters` no longer constructs a `Patcher` as an import-time side effect (which pulled in maxref, layout, etc.). The default-attribute set is now computed lazily on first use and cached. Also removed a dead `_infer_category` helper and an unreachable `NotImplementedError` guard (the caller strips subpatcher dicts before that path).
|
|
101
|
+
|
|
102
|
+
### Removed: Honest CLI help for the moved `serve` / `repl` subcommands
|
|
103
|
+
|
|
104
|
+
- The `serve` and `repl` subcommands (whose implementations moved to `py2max-server` in 0.3.0) no longer advertise themselves as live in `--help`; they now removed.
|
|
105
|
+
|
|
106
|
+
### Removed: MaxRefDB deprecated alias methods
|
|
107
|
+
|
|
108
|
+
- 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.
|
|
109
|
+
- 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.
|
|
110
|
+
|
|
111
|
+
### Removed: Obsolete layout experiments and vendored editor assets
|
|
112
|
+
|
|
113
|
+
- 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`.
|
|
114
|
+
- 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.
|
|
115
|
+
- 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.
|
|
116
|
+
|
|
117
|
+
## [0.3.1]
|
|
118
|
+
|
|
119
|
+
### New: Standalone `gen.codebox~` Support
|
|
120
|
+
|
|
121
|
+
- `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.
|
|
122
|
+
- 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.
|
|
123
|
+
- 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.
|
|
124
|
+
- 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.
|
|
125
|
+
|
|
5
126
|
## [0.3.0]
|
|
6
127
|
|
|
7
128
|
### Removed: Interactive Server Split Into `py2max-server` (breaking)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2max
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A library for offline generation of Max/MSP patcher (.maxpat) files.
|
|
5
5
|
Keywords: Max,maxpat,offline
|
|
6
6
|
Author: Shakeeb Alireza
|
|
@@ -21,6 +21,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Operating System :: OS Independent
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Dist: graph-layout>=0.4 ; extra == 'graph'
|
|
25
|
+
Requires-Dist: ogdf-py>=0.3 ; python_full_version >= '3.10' and extra == 'graph'
|
|
26
|
+
Requires-Dist: hola-graph>=0.1 ; extra == 'graph'
|
|
24
27
|
Maintainer: Shakeeb Alireza
|
|
25
28
|
Maintainer-email: Shakeeb Alireza <shakfu@users.noreply.github.com>
|
|
26
29
|
Requires-Python: >=3.9
|
|
@@ -29,6 +32,7 @@ Project-URL: Documentation, https://github.com/shakfu/py2max#readme
|
|
|
29
32
|
Project-URL: Repository, https://github.com/shakfu/py2max.git
|
|
30
33
|
Project-URL: Issues, https://github.com/shakfu/py2max/issues
|
|
31
34
|
Project-URL: Changelog, https://github.com/shakfu/py2max/blob/main/CHANGELOG.md
|
|
35
|
+
Provides-Extra: graph
|
|
32
36
|
Description-Content-Type: text/markdown
|
|
33
37
|
|
|
34
38
|
# py2max
|
|
@@ -84,8 +88,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
|
84
88
|
|
|
85
89
|
- **Offline Patch Generation** - Create Max patches programmatically without Max running
|
|
86
90
|
- **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
|
|
91
|
+
- **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
|
|
87
92
|
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
88
|
-
- **
|
|
93
|
+
- **Fully typed** - Passes `mypy --strict`; no runtime dependencies
|
|
94
|
+
- **High Test Coverage** - 420+ tests ensure reliability
|
|
95
|
+
|
|
96
|
+
### Max for Live (.amxd)
|
|
97
|
+
|
|
98
|
+
Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
|
|
99
|
+
auto-detect the `.amxd` extension and read/write the binary device format,
|
|
100
|
+
byte-for-byte compatible with Max-exported devices.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from py2max import Patcher
|
|
104
|
+
|
|
105
|
+
# device_type: "audio_effect" (default), "instrument", or "midi_effect"
|
|
106
|
+
p = Patcher('gain.amxd', device_type='audio_effect')
|
|
107
|
+
p.enable_presentation(devicewidth=120) # render Ableton's device strip
|
|
108
|
+
|
|
109
|
+
plugin = p.add_textbox('plugin~') # audio in from Live
|
|
110
|
+
gain = p.add('live.gain~', maxclass='live.gain~')
|
|
111
|
+
plugout = p.add_textbox('plugout~') # audio back to Live
|
|
112
|
+
gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
|
|
113
|
+
|
|
114
|
+
p.add_line(plugin, gain, outlet=0, inlet=0)
|
|
115
|
+
p.add_line(gain, plugout, outlet=0, inlet=0)
|
|
116
|
+
p.save() # writes a binary .amxd
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Helpers: `Patcher.enable_presentation(devicewidth=...)`,
|
|
120
|
+
`Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
|
|
121
|
+
rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
|
|
122
|
+
binary helpers live in `py2max.m4l`.
|
|
89
123
|
|
|
90
124
|
### Interactive Server (separate package)
|
|
91
125
|
|
|
@@ -142,7 +176,7 @@ p.save()
|
|
|
142
176
|
|
|
143
177
|
### MaxRef Integration
|
|
144
178
|
|
|
145
|
-
Access documentation for
|
|
179
|
+
Access documentation for 1175 Max objects:
|
|
146
180
|
|
|
147
181
|
```python
|
|
148
182
|
p = Patcher('demo.maxpat')
|
|
@@ -186,10 +220,10 @@ osc = p.find_by_id('cycle_1')
|
|
|
186
220
|
Query Max object metadata efficiently:
|
|
187
221
|
|
|
188
222
|
```python
|
|
189
|
-
from py2max import MaxRefDB
|
|
223
|
+
from py2max.maxref import MaxRefDB
|
|
190
224
|
|
|
191
225
|
db = MaxRefDB() # Auto-cached on first use
|
|
192
|
-
print(len(db)) #
|
|
226
|
+
print(len(db)) # 1175 objects
|
|
193
227
|
|
|
194
228
|
if 'cycle~' in db:
|
|
195
229
|
info = db['cycle~']
|
|
@@ -252,6 +286,34 @@ p.link(sbox, dac)
|
|
|
252
286
|
p.save()
|
|
253
287
|
```
|
|
254
288
|
|
|
289
|
+
### Gen Codebox
|
|
290
|
+
|
|
291
|
+
`add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
|
|
292
|
+
patch in a single box that sits directly in a regular Max patcher (unlike the
|
|
293
|
+
inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
|
|
294
|
+
subpatcher). Inlet/outlet counts are derived automatically from the highest
|
|
295
|
+
`inN`/`outN` references in the code:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
p = Patcher('fbdelay.maxpat')
|
|
299
|
+
|
|
300
|
+
# 1 inlet (in1), 1 outlet (out1)
|
|
301
|
+
osc = p.add('cycle~ 440')
|
|
302
|
+
cb = p.add_gen_codebox('''
|
|
303
|
+
Param feedback(0.5, min=0.0, max=0.95);
|
|
304
|
+
History fb(0.0);
|
|
305
|
+
out1 = in1 + fb * feedback;
|
|
306
|
+
fb = out1;
|
|
307
|
+
''')
|
|
308
|
+
dac = p.add('ezdac~')
|
|
309
|
+
p.link(osc, cb)
|
|
310
|
+
p.link(cb, dac)
|
|
311
|
+
p.save()
|
|
312
|
+
|
|
313
|
+
# Or via the add() string shortcut (single-line / `;`-terminated code)
|
|
314
|
+
cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
|
|
315
|
+
```
|
|
316
|
+
|
|
255
317
|
### Object Search
|
|
256
318
|
|
|
257
319
|
```python
|
|
@@ -51,8 +51,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
|
51
51
|
|
|
52
52
|
- **Offline Patch Generation** - Create Max patches programmatically without Max running
|
|
53
53
|
- **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
|
|
54
|
+
- **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
|
|
54
55
|
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
55
|
-
- **
|
|
56
|
+
- **Fully typed** - Passes `mypy --strict`; no runtime dependencies
|
|
57
|
+
- **High Test Coverage** - 420+ tests ensure reliability
|
|
58
|
+
|
|
59
|
+
### Max for Live (.amxd)
|
|
60
|
+
|
|
61
|
+
Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
|
|
62
|
+
auto-detect the `.amxd` extension and read/write the binary device format,
|
|
63
|
+
byte-for-byte compatible with Max-exported devices.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from py2max import Patcher
|
|
67
|
+
|
|
68
|
+
# device_type: "audio_effect" (default), "instrument", or "midi_effect"
|
|
69
|
+
p = Patcher('gain.amxd', device_type='audio_effect')
|
|
70
|
+
p.enable_presentation(devicewidth=120) # render Ableton's device strip
|
|
71
|
+
|
|
72
|
+
plugin = p.add_textbox('plugin~') # audio in from Live
|
|
73
|
+
gain = p.add('live.gain~', maxclass='live.gain~')
|
|
74
|
+
plugout = p.add_textbox('plugout~') # audio back to Live
|
|
75
|
+
gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
|
|
76
|
+
|
|
77
|
+
p.add_line(plugin, gain, outlet=0, inlet=0)
|
|
78
|
+
p.add_line(gain, plugout, outlet=0, inlet=0)
|
|
79
|
+
p.save() # writes a binary .amxd
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Helpers: `Patcher.enable_presentation(devicewidth=...)`,
|
|
83
|
+
`Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
|
|
84
|
+
rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
|
|
85
|
+
binary helpers live in `py2max.m4l`.
|
|
56
86
|
|
|
57
87
|
### Interactive Server (separate package)
|
|
58
88
|
|
|
@@ -109,7 +139,7 @@ p.save()
|
|
|
109
139
|
|
|
110
140
|
### MaxRef Integration
|
|
111
141
|
|
|
112
|
-
Access documentation for
|
|
142
|
+
Access documentation for 1175 Max objects:
|
|
113
143
|
|
|
114
144
|
```python
|
|
115
145
|
p = Patcher('demo.maxpat')
|
|
@@ -153,10 +183,10 @@ osc = p.find_by_id('cycle_1')
|
|
|
153
183
|
Query Max object metadata efficiently:
|
|
154
184
|
|
|
155
185
|
```python
|
|
156
|
-
from py2max import MaxRefDB
|
|
186
|
+
from py2max.maxref import MaxRefDB
|
|
157
187
|
|
|
158
188
|
db = MaxRefDB() # Auto-cached on first use
|
|
159
|
-
print(len(db)) #
|
|
189
|
+
print(len(db)) # 1175 objects
|
|
160
190
|
|
|
161
191
|
if 'cycle~' in db:
|
|
162
192
|
info = db['cycle~']
|
|
@@ -219,6 +249,34 @@ p.link(sbox, dac)
|
|
|
219
249
|
p.save()
|
|
220
250
|
```
|
|
221
251
|
|
|
252
|
+
### Gen Codebox
|
|
253
|
+
|
|
254
|
+
`add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
|
|
255
|
+
patch in a single box that sits directly in a regular Max patcher (unlike the
|
|
256
|
+
inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
|
|
257
|
+
subpatcher). Inlet/outlet counts are derived automatically from the highest
|
|
258
|
+
`inN`/`outN` references in the code:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
p = Patcher('fbdelay.maxpat')
|
|
262
|
+
|
|
263
|
+
# 1 inlet (in1), 1 outlet (out1)
|
|
264
|
+
osc = p.add('cycle~ 440')
|
|
265
|
+
cb = p.add_gen_codebox('''
|
|
266
|
+
Param feedback(0.5, min=0.0, max=0.95);
|
|
267
|
+
History fb(0.0);
|
|
268
|
+
out1 = in1 + fb * feedback;
|
|
269
|
+
fb = out1;
|
|
270
|
+
''')
|
|
271
|
+
dac = p.add('ezdac~')
|
|
272
|
+
p.link(osc, cb)
|
|
273
|
+
p.link(cb, dac)
|
|
274
|
+
p.save()
|
|
275
|
+
|
|
276
|
+
# Or via the add() string shortcut (single-line / `;`-terminated code)
|
|
277
|
+
cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
|
|
278
|
+
```
|
|
279
|
+
|
|
222
280
|
### Object Search
|
|
223
281
|
|
|
224
282
|
```python
|
|
@@ -148,6 +148,7 @@ def _generate_test_source(name: str, data: Dict[str, Any]) -> str:
|
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def cmd_new(args: argparse.Namespace) -> int:
|
|
151
|
+
"""Create a new, empty patcher file at the given path."""
|
|
151
152
|
path = Path(args.path)
|
|
152
153
|
if path.exists() and not args.force:
|
|
153
154
|
print(f"Refusing to overwrite existing file: {path}", file=sys.stderr)
|
|
@@ -179,6 +180,7 @@ def cmd_new(args: argparse.Namespace) -> int:
|
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
def cmd_info(args: argparse.Namespace) -> int:
|
|
183
|
+
"""Print a summary of a patcher file (object and connection counts)."""
|
|
182
184
|
path = Path(args.path)
|
|
183
185
|
patcher = Patcher.from_file(path)
|
|
184
186
|
_coerce_rect(patcher)
|
|
@@ -201,6 +203,7 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
201
203
|
|
|
202
204
|
|
|
203
205
|
def cmd_optimize(args: argparse.Namespace) -> int:
|
|
206
|
+
"""Re-run layout optimization on a patcher and write the result."""
|
|
204
207
|
input_path = Path(args.input)
|
|
205
208
|
save_path = Path(args.output) if args.output else input_path
|
|
206
209
|
|
|
@@ -222,6 +225,7 @@ def cmd_optimize(args: argparse.Namespace) -> int:
|
|
|
222
225
|
|
|
223
226
|
|
|
224
227
|
def cmd_validate(args: argparse.Namespace) -> int:
|
|
228
|
+
"""Validate a patcher's connections against maxref inlet/outlet metadata."""
|
|
225
229
|
path = Path(args.path)
|
|
226
230
|
patcher = Patcher.from_file(path)
|
|
227
231
|
_coerce_rect(patcher)
|
|
@@ -272,6 +276,7 @@ def _parse_transform_spec(spec: str) -> tuple[str, str | None]:
|
|
|
272
276
|
|
|
273
277
|
|
|
274
278
|
def cmd_transform(args: argparse.Namespace) -> int:
|
|
279
|
+
"""Apply one or more transformers to a patcher (or list available ones)."""
|
|
275
280
|
if args.list_transformers:
|
|
276
281
|
for name, desc in available_transformers().items():
|
|
277
282
|
print(f"{name}: {desc}")
|
|
@@ -316,6 +321,7 @@ def cmd_transform(args: argparse.Namespace) -> int:
|
|
|
316
321
|
|
|
317
322
|
|
|
318
323
|
def cmd_convert(args: argparse.Namespace) -> int:
|
|
324
|
+
"""Convert between formats (e.g. .maxpat to Python, maxref to SQLite)."""
|
|
319
325
|
if args.mode == "maxpat-to-python":
|
|
320
326
|
if not args.input or not args.output:
|
|
321
327
|
print(
|
|
@@ -636,19 +642,6 @@ def cmd_db_cache(args: argparse.Namespace) -> int:
|
|
|
636
642
|
return 1
|
|
637
643
|
|
|
638
644
|
|
|
639
|
-
def cmd_serve(args: argparse.Namespace) -> int:
|
|
640
|
-
"""The interactive server moved to the separate py2max-server package."""
|
|
641
|
-
target = getattr(args, "input", None) or "<patch.maxpat>"
|
|
642
|
-
print(
|
|
643
|
-
"The interactive server has moved to the separate 'py2max-server' package.\n"
|
|
644
|
-
"Install it and use its CLI instead:\n"
|
|
645
|
-
" pip install py2max-server\n"
|
|
646
|
-
f" py2max-server serve {target}",
|
|
647
|
-
file=sys.stderr,
|
|
648
|
-
)
|
|
649
|
-
return 1
|
|
650
|
-
|
|
651
|
-
|
|
652
645
|
def cmd_preview(args: argparse.Namespace) -> int:
|
|
653
646
|
"""Generate SVG preview of a patcher."""
|
|
654
647
|
import tempfile
|
|
@@ -695,20 +688,8 @@ def cmd_preview(args: argparse.Namespace) -> int:
|
|
|
695
688
|
return 1
|
|
696
689
|
|
|
697
690
|
|
|
698
|
-
def cmd_repl(args: argparse.Namespace) -> int:
|
|
699
|
-
"""The remote REPL moved to the separate py2max-server package."""
|
|
700
|
-
target = getattr(args, "server", None) or "localhost:9000"
|
|
701
|
-
print(
|
|
702
|
-
"The remote REPL has moved to the separate 'py2max-server' package.\n"
|
|
703
|
-
"Install it and use its CLI instead:\n"
|
|
704
|
-
" pip install py2max-server\n"
|
|
705
|
-
f" py2max-server repl {target}",
|
|
706
|
-
file=sys.stderr,
|
|
707
|
-
)
|
|
708
|
-
return 1
|
|
709
|
-
|
|
710
|
-
|
|
711
691
|
def cmd_maxref(args: argparse.Namespace) -> int:
|
|
692
|
+
"""Query the Max object reference (list objects or show one's details)."""
|
|
712
693
|
cache = MaxRefCache()
|
|
713
694
|
|
|
714
695
|
if args.list:
|
|
@@ -783,6 +764,7 @@ def cmd_maxref(args: argparse.Namespace) -> int:
|
|
|
783
764
|
|
|
784
765
|
|
|
785
766
|
def build_parser() -> argparse.ArgumentParser:
|
|
767
|
+
"""Construct the top-level argument parser with all subcommands."""
|
|
786
768
|
parser = argparse.ArgumentParser(
|
|
787
769
|
prog="py2max", description="Utilities for working with Max patchers."
|
|
788
770
|
)
|
|
@@ -836,33 +818,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
836
818
|
val_parser.add_argument("path", help="Target .maxpat path")
|
|
837
819
|
val_parser.set_defaults(func=cmd_validate)
|
|
838
820
|
|
|
839
|
-
serve_parser = subparsers.add_parser(
|
|
840
|
-
"serve", help="Start interactive server with live preview"
|
|
841
|
-
)
|
|
842
|
-
serve_parser.add_argument("input", help="Input .maxpat file")
|
|
843
|
-
serve_parser.add_argument(
|
|
844
|
-
"--port",
|
|
845
|
-
type=int,
|
|
846
|
-
default=8000,
|
|
847
|
-
help="HTTP server port (default: 8000, WebSocket on port+1)",
|
|
848
|
-
)
|
|
849
|
-
serve_parser.add_argument(
|
|
850
|
-
"--no-open", action="store_true", help="Don't automatically open browser"
|
|
851
|
-
)
|
|
852
|
-
serve_parser.add_argument(
|
|
853
|
-
"--no-save", action="store_true", help="Disable auto-save on changes"
|
|
854
|
-
)
|
|
855
|
-
serve_parser.add_argument(
|
|
856
|
-
"--repl",
|
|
857
|
-
action="store_true",
|
|
858
|
-
help="Start interactive REPL for live patch editing",
|
|
859
|
-
)
|
|
860
|
-
serve_parser.add_argument(
|
|
861
|
-
"--log-file",
|
|
862
|
-
help="Redirect server logs to file (enables single-terminal REPL mode when used with --repl)",
|
|
863
|
-
)
|
|
864
|
-
serve_parser.set_defaults(func=cmd_serve)
|
|
865
|
-
|
|
866
821
|
preview_parser = subparsers.add_parser(
|
|
867
822
|
"preview", help="Generate SVG preview of a patcher"
|
|
868
823
|
)
|
|
@@ -1094,26 +1049,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1094
1049
|
)
|
|
1095
1050
|
cache_clear.set_defaults(func=cmd_db)
|
|
1096
1051
|
|
|
1097
|
-
# REPL client command
|
|
1098
|
-
repl_parser = subparsers.add_parser("repl", help="Connect to remote REPL server")
|
|
1099
|
-
repl_parser.add_argument(
|
|
1100
|
-
"server",
|
|
1101
|
-
nargs="?",
|
|
1102
|
-
default="localhost:9000",
|
|
1103
|
-
help="Server address (default: localhost:9000)",
|
|
1104
|
-
)
|
|
1105
|
-
repl_parser.add_argument(
|
|
1106
|
-
"--token",
|
|
1107
|
-
default=None,
|
|
1108
|
-
help="Session token printed by the server "
|
|
1109
|
-
"(or set PY2MAX_REPL_TOKEN). Required for authentication.",
|
|
1110
|
-
)
|
|
1111
|
-
repl_parser.set_defaults(func=cmd_repl)
|
|
1112
|
-
|
|
1113
1052
|
return parser
|
|
1114
1053
|
|
|
1115
1054
|
|
|
1116
1055
|
def main(argv: List[str] | None = None) -> int:
|
|
1056
|
+
"""CLI entry point: parse ``argv`` and dispatch to the subcommand handler."""
|
|
1117
1057
|
parser = build_parser()
|
|
1118
1058
|
args = parser.parse_args(argv)
|
|
1119
1059
|
|
|
@@ -60,6 +60,8 @@ class AbstractBox(ABC):
|
|
|
60
60
|
id: Optional[str]
|
|
61
61
|
maxclass: str
|
|
62
62
|
patching_rect: Rect
|
|
63
|
+
numinlets: int
|
|
64
|
+
numoutlets: int
|
|
63
65
|
_kwds: Dict[str, Any]
|
|
64
66
|
|
|
65
67
|
@abstractmethod
|
|
@@ -135,8 +137,6 @@ class AbstractPatcher(ABC):
|
|
|
135
137
|
_lines: list[AbstractPatchline]
|
|
136
138
|
_edge_ids: list[tuple[str, str]]
|
|
137
139
|
_id_counter: int = 0
|
|
138
|
-
_link_counter: int = 0
|
|
139
|
-
_last_link: Optional[tuple[str, str]]
|
|
140
140
|
_reset_on_render: bool
|
|
141
141
|
_flow_direction: str
|
|
142
142
|
_cluster_connected: bool
|