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