napari-padbound 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napari_padbound-0.2.0/.gitignore +1 -0
- napari_padbound-0.2.0/LICENSE +29 -0
- napari_padbound-0.2.0/PKG-INFO +151 -0
- napari_padbound-0.2.0/README.md +109 -0
- napari_padbound-0.2.0/pyproject.toml +96 -0
- napari_padbound-0.2.0/src/napari_padbound/__init__.py +27 -0
- napari_padbound-0.2.0/src/napari_padbound/_tests/__init__.py +0 -0
- napari_padbound-0.2.0/src/napari_padbound/_tests/test_widget.py +7 -0
- napari_padbound-0.2.0/src/napari_padbound/_version.py +34 -0
- napari_padbound-0.2.0/src/napari_padbound/control_mapper.py +155 -0
- napari_padbound-0.2.0/src/napari_padbound/label_feedback.py +254 -0
- napari_padbound-0.2.0/src/napari_padbound/napari.yaml +12 -0
- napari_padbound-0.2.0/src/napari_padbound/viewer_controller.py +424 -0
- napari_padbound-0.2.0/src/napari_padbound/widget.py +121 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
launch_napari.py
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Utz H. Ermel
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: napari-padbound
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A napari plugin for padbound
|
|
5
|
+
Project-URL: Bug Tracker, https://github.com/uermel/napari-padbound/issues
|
|
6
|
+
Project-URL: Documentation, https://github.com/uermel/napari-padbound#README.md
|
|
7
|
+
Project-URL: Source Code, https://github.com/uermel/napari-padbound
|
|
8
|
+
Author-email: "Utz H. Ermel" <utz@ermel.me>
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: napari
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: magicgui
|
|
26
|
+
Requires-Dist: napari
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: padbound>=0.3.0
|
|
29
|
+
Requires-Dist: qtpy
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
34
|
+
Provides-Extra: testing
|
|
35
|
+
Requires-Dist: napari; extra == 'testing'
|
|
36
|
+
Requires-Dist: pyqt5; extra == 'testing'
|
|
37
|
+
Requires-Dist: pytest; extra == 'testing'
|
|
38
|
+
Requires-Dist: pytest-cov; extra == 'testing'
|
|
39
|
+
Requires-Dist: pytest-qt; extra == 'testing'
|
|
40
|
+
Requires-Dist: tox; extra == 'testing'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# napari-padbound
|
|
44
|
+
|
|
45
|
+
[](https://github.com/uermel/napari-padbound/raw/main/LICENSE)
|
|
46
|
+
[](https://pypi.org/project/napari-padbound)
|
|
47
|
+
[](https://python.org)
|
|
48
|
+
[](https://napari-hub.org/plugins/napari-padbound)
|
|
49
|
+
|
|
50
|
+
A [napari] plugin for controlling image annotation workflows with MIDI controllers via [padbound].
|
|
51
|
+
|
|
52
|
+
Use physical pads, knobs, faders, and buttons to navigate slices, select labels, adjust brush size, zoom, undo/redo, and more — with real-time LED feedback showing your current label colors on the controller.
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- **Auto-detection** — Automatically finds and connects to any [padbound]-supported MIDI controller
|
|
57
|
+
- **Smart control mapping** — Automatically assigns available hardware controls to napari functions based on controller capabilities
|
|
58
|
+
- **Slice navigation** — Coarse and fine navigation through 3D+ data volumes via faders or knobs
|
|
59
|
+
- **Slice stepping** — Increment/decrement slices one at a time via navigation buttons
|
|
60
|
+
- **Zoom control** — Logarithmic zoom mapping (0.01x–10x) via knobs or faders
|
|
61
|
+
- **Brush size control** — Logarithmic brush size adjustment (1–100px) for label painting
|
|
62
|
+
- **Label selection** — Select labels by pressing pads; pad 1 is the eraser, remaining pads map to labels 1, 2, 3, ...
|
|
63
|
+
- **LED color feedback** — Pads display actual label colors from the napari colormap, with the selected label pulsing (on RGB-capable controllers)
|
|
64
|
+
- **Dimension rolling** — Cycle through dimension views (XY, YZ, XZ) via navigation buttons
|
|
65
|
+
- **Undo/redo** — Transport buttons for edit history on the active Labels layer
|
|
66
|
+
- **Graceful degradation** — Three feedback strategies (RGB color, binary toggle, none) adapt automatically to controller capabilities
|
|
67
|
+
|
|
68
|
+
## Supported Controllers
|
|
69
|
+
|
|
70
|
+
Any controller with a [padbound] plugin works automatically. Currently supported:
|
|
71
|
+
|
|
72
|
+
| Controller | Best for | Key controls |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| **AKAI APC mini MK2** | Full RGB feedback, many faders | 64 RGB pads, 9 faders, 17 buttons |
|
|
75
|
+
| **AKAI LPD8 MK2** | Compact RGB + knobs | 8 RGB pads, 8 knobs, 4 banks |
|
|
76
|
+
| **AKAI MPD218** | Velocity-sensitive pads | 16 pads, 6 encoders, 3 banks |
|
|
77
|
+
| **PreSonus ATOM** | RGB pads + encoders + buttons | 16 RGB pads, 4 encoders, 20 buttons |
|
|
78
|
+
| **Synido TempoPad P16** | RGB pads + transport | 16 RGB pads, 4 encoders, 6 buttons |
|
|
79
|
+
| **Behringer X-Touch Mini** | Encoders with LED rings | 16 pads, 8 encoders, 1 fader |
|
|
80
|
+
| **Xjam** | Budget option, multi-bank | 16 pads, 6 knobs, 3 banks |
|
|
81
|
+
|
|
82
|
+
## How Control Mapping Works
|
|
83
|
+
|
|
84
|
+
The plugin automatically discovers available controls and assigns them by priority:
|
|
85
|
+
|
|
86
|
+
**Continuous controls** (assigned in order: faders first, then knobs, then encoders):
|
|
87
|
+
1. First control → **Coarse slice** (full range of the data volume)
|
|
88
|
+
2. Second control → **Fine slice** (±64 slices around the coarse position)
|
|
89
|
+
3. Third control → **Brush size** (logarithmic, 1–100px)
|
|
90
|
+
4. Fourth control → **Zoom** (logarithmic, 0.01x–10x)
|
|
91
|
+
|
|
92
|
+
**Pads** → **Label selection** (pad 1 = eraser, pad 2+ = labels)
|
|
93
|
+
|
|
94
|
+
**Navigation buttons** → Up/Down = slice step, Left/Right = dimension roll
|
|
95
|
+
|
|
96
|
+
**Transport buttons** → Stop = undo, Play = redo
|
|
97
|
+
|
|
98
|
+
The widget displays the detected controller and its mapped controls so you can see what each physical control does.
|
|
99
|
+
|
|
100
|
+
## Installation
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install napari-padbound
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For development:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/uermel/napari-padbound.git
|
|
110
|
+
cd napari-padbound
|
|
111
|
+
pip install -e ".[dev,testing]"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
1. Connect a supported MIDI controller via USB
|
|
117
|
+
2. Open [napari]
|
|
118
|
+
3. Go to **Plugins > padbound** to open the widget
|
|
119
|
+
4. Load a 3D image and create a Labels layer
|
|
120
|
+
5. Use your controller to navigate slices, select labels, and annotate
|
|
121
|
+
|
|
122
|
+
The widget shows the connected controller name and the mapping of physical controls to napari functions. If no controller is detected, the widget will indicate this.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Linting
|
|
128
|
+
ruff check src/
|
|
129
|
+
ruff format src/
|
|
130
|
+
black src/
|
|
131
|
+
|
|
132
|
+
# Run tests
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Contributing
|
|
137
|
+
|
|
138
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
Distributed under the terms of the [BSD-3] license, napari-padbound is free and open source software.
|
|
143
|
+
|
|
144
|
+
## Issues
|
|
145
|
+
|
|
146
|
+
If you encounter any problems, please [file an issue] along with a detailed description.
|
|
147
|
+
|
|
148
|
+
[napari]: https://github.com/napari/napari
|
|
149
|
+
[padbound]: https://github.com/uermel/padbound
|
|
150
|
+
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
|
|
151
|
+
[file an issue]: https://github.com/uermel/napari-padbound/issues
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# napari-padbound
|
|
2
|
+
|
|
3
|
+
[](https://github.com/uermel/napari-padbound/raw/main/LICENSE)
|
|
4
|
+
[](https://pypi.org/project/napari-padbound)
|
|
5
|
+
[](https://python.org)
|
|
6
|
+
[](https://napari-hub.org/plugins/napari-padbound)
|
|
7
|
+
|
|
8
|
+
A [napari] plugin for controlling image annotation workflows with MIDI controllers via [padbound].
|
|
9
|
+
|
|
10
|
+
Use physical pads, knobs, faders, and buttons to navigate slices, select labels, adjust brush size, zoom, undo/redo, and more — with real-time LED feedback showing your current label colors on the controller.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Auto-detection** — Automatically finds and connects to any [padbound]-supported MIDI controller
|
|
15
|
+
- **Smart control mapping** — Automatically assigns available hardware controls to napari functions based on controller capabilities
|
|
16
|
+
- **Slice navigation** — Coarse and fine navigation through 3D+ data volumes via faders or knobs
|
|
17
|
+
- **Slice stepping** — Increment/decrement slices one at a time via navigation buttons
|
|
18
|
+
- **Zoom control** — Logarithmic zoom mapping (0.01x–10x) via knobs or faders
|
|
19
|
+
- **Brush size control** — Logarithmic brush size adjustment (1–100px) for label painting
|
|
20
|
+
- **Label selection** — Select labels by pressing pads; pad 1 is the eraser, remaining pads map to labels 1, 2, 3, ...
|
|
21
|
+
- **LED color feedback** — Pads display actual label colors from the napari colormap, with the selected label pulsing (on RGB-capable controllers)
|
|
22
|
+
- **Dimension rolling** — Cycle through dimension views (XY, YZ, XZ) via navigation buttons
|
|
23
|
+
- **Undo/redo** — Transport buttons for edit history on the active Labels layer
|
|
24
|
+
- **Graceful degradation** — Three feedback strategies (RGB color, binary toggle, none) adapt automatically to controller capabilities
|
|
25
|
+
|
|
26
|
+
## Supported Controllers
|
|
27
|
+
|
|
28
|
+
Any controller with a [padbound] plugin works automatically. Currently supported:
|
|
29
|
+
|
|
30
|
+
| Controller | Best for | Key controls |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **AKAI APC mini MK2** | Full RGB feedback, many faders | 64 RGB pads, 9 faders, 17 buttons |
|
|
33
|
+
| **AKAI LPD8 MK2** | Compact RGB + knobs | 8 RGB pads, 8 knobs, 4 banks |
|
|
34
|
+
| **AKAI MPD218** | Velocity-sensitive pads | 16 pads, 6 encoders, 3 banks |
|
|
35
|
+
| **PreSonus ATOM** | RGB pads + encoders + buttons | 16 RGB pads, 4 encoders, 20 buttons |
|
|
36
|
+
| **Synido TempoPad P16** | RGB pads + transport | 16 RGB pads, 4 encoders, 6 buttons |
|
|
37
|
+
| **Behringer X-Touch Mini** | Encoders with LED rings | 16 pads, 8 encoders, 1 fader |
|
|
38
|
+
| **Xjam** | Budget option, multi-bank | 16 pads, 6 knobs, 3 banks |
|
|
39
|
+
|
|
40
|
+
## How Control Mapping Works
|
|
41
|
+
|
|
42
|
+
The plugin automatically discovers available controls and assigns them by priority:
|
|
43
|
+
|
|
44
|
+
**Continuous controls** (assigned in order: faders first, then knobs, then encoders):
|
|
45
|
+
1. First control → **Coarse slice** (full range of the data volume)
|
|
46
|
+
2. Second control → **Fine slice** (±64 slices around the coarse position)
|
|
47
|
+
3. Third control → **Brush size** (logarithmic, 1–100px)
|
|
48
|
+
4. Fourth control → **Zoom** (logarithmic, 0.01x–10x)
|
|
49
|
+
|
|
50
|
+
**Pads** → **Label selection** (pad 1 = eraser, pad 2+ = labels)
|
|
51
|
+
|
|
52
|
+
**Navigation buttons** → Up/Down = slice step, Left/Right = dimension roll
|
|
53
|
+
|
|
54
|
+
**Transport buttons** → Stop = undo, Play = redo
|
|
55
|
+
|
|
56
|
+
The widget displays the detected controller and its mapped controls so you can see what each physical control does.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install napari-padbound
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For development:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/uermel/napari-padbound.git
|
|
68
|
+
cd napari-padbound
|
|
69
|
+
pip install -e ".[dev,testing]"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
1. Connect a supported MIDI controller via USB
|
|
75
|
+
2. Open [napari]
|
|
76
|
+
3. Go to **Plugins > padbound** to open the widget
|
|
77
|
+
4. Load a 3D image and create a Labels layer
|
|
78
|
+
5. Use your controller to navigate slices, select labels, and annotate
|
|
79
|
+
|
|
80
|
+
The widget shows the connected controller name and the mapping of physical controls to napari functions. If no controller is detected, the widget will indicate this.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Linting
|
|
86
|
+
ruff check src/
|
|
87
|
+
ruff format src/
|
|
88
|
+
black src/
|
|
89
|
+
|
|
90
|
+
# Run tests
|
|
91
|
+
pytest
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
Distributed under the terms of the [BSD-3] license, napari-padbound is free and open source software.
|
|
101
|
+
|
|
102
|
+
## Issues
|
|
103
|
+
|
|
104
|
+
If you encounter any problems, please [file an issue] along with a detailed description.
|
|
105
|
+
|
|
106
|
+
[napari]: https://github.com/napari/napari
|
|
107
|
+
[padbound]: https://github.com/uermel/padbound
|
|
108
|
+
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
|
|
109
|
+
[file an issue]: https://github.com/uermel/napari-padbound/issues
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "napari-padbound"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A napari plugin for padbound"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Utz H. Ermel", email = "utz@ermel.me" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Framework :: napari",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: BSD License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"napari",
|
|
32
|
+
"numpy",
|
|
33
|
+
"magicgui",
|
|
34
|
+
"qtpy",
|
|
35
|
+
"padbound>=0.3.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
testing = [
|
|
40
|
+
"tox",
|
|
41
|
+
"pytest",
|
|
42
|
+
"pytest-cov",
|
|
43
|
+
"pytest-qt",
|
|
44
|
+
"napari",
|
|
45
|
+
"pyqt5",
|
|
46
|
+
]
|
|
47
|
+
dev = [
|
|
48
|
+
"black",
|
|
49
|
+
"ruff",
|
|
50
|
+
"pre-commit",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.entry-points."napari.manifest"]
|
|
54
|
+
napari-padbound = "napari_padbound:napari.yaml"
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
"Bug Tracker" = "https://github.com/uermel/napari-padbound/issues"
|
|
58
|
+
"Documentation" = "https://github.com/uermel/napari-padbound#README.md"
|
|
59
|
+
"Source Code" = "https://github.com/uermel/napari-padbound"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.version]
|
|
62
|
+
source = "vcs"
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.hooks.vcs]
|
|
65
|
+
version-file = "src/napari_padbound/_version.py"
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.sdist]
|
|
68
|
+
include = ["/src"]
|
|
69
|
+
|
|
70
|
+
[tool.hatch.build.targets.wheel]
|
|
71
|
+
packages = ["src/napari_padbound"]
|
|
72
|
+
|
|
73
|
+
[tool.black]
|
|
74
|
+
line-length = 120
|
|
75
|
+
target-version = ["py311"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
line-length = 120
|
|
79
|
+
lint.select = [
|
|
80
|
+
"E", # pycodestyle errors
|
|
81
|
+
"W", # pycodestyle warnings
|
|
82
|
+
"F", # Pyflakes
|
|
83
|
+
"B", # flake8-bugbear
|
|
84
|
+
"I", # isort
|
|
85
|
+
]
|
|
86
|
+
lint.ignore = [
|
|
87
|
+
"E501", # line too long (handled by black)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.pytest.ini_options]
|
|
91
|
+
minversion = "6.0"
|
|
92
|
+
addopts = "-v --color=yes"
|
|
93
|
+
testpaths = ["src/napari_padbound/_tests"]
|
|
94
|
+
filterwarnings = [
|
|
95
|
+
"ignore::DeprecationWarning",
|
|
96
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ._version import version as __version__
|
|
3
|
+
except ImportError:
|
|
4
|
+
__version__ = "unknown"
|
|
5
|
+
|
|
6
|
+
from .control_mapper import ControlMapper, ControlMapping
|
|
7
|
+
from .label_feedback import (
|
|
8
|
+
LabelFeedbackStrategy,
|
|
9
|
+
NoFeedbackStrategy,
|
|
10
|
+
RGBColorStrategy,
|
|
11
|
+
ToggleStrategy,
|
|
12
|
+
create_feedback_strategy,
|
|
13
|
+
)
|
|
14
|
+
from .viewer_controller import ViewerController
|
|
15
|
+
from .widget import PadboundWidget
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"ControlMapper",
|
|
19
|
+
"ControlMapping",
|
|
20
|
+
"LabelFeedbackStrategy",
|
|
21
|
+
"NoFeedbackStrategy",
|
|
22
|
+
"RGBColorStrategy",
|
|
23
|
+
"ToggleStrategy",
|
|
24
|
+
"create_feedback_strategy",
|
|
25
|
+
"ViewerController",
|
|
26
|
+
"PadboundWidget",
|
|
27
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Control mapping for auto-discovering and assigning MIDI controls to features."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from padbound import Controller
|
|
11
|
+
from padbound.controls import ControlDefinition
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ControlMapping(BaseModel):
|
|
15
|
+
"""Mapping of physical controls to napari features."""
|
|
16
|
+
|
|
17
|
+
coarse_slice: str | None = None # control_id for coarse slice
|
|
18
|
+
fine_slice: str | None = None # control_id for fine slice
|
|
19
|
+
zoom: str | None = None # control_id for zoom
|
|
20
|
+
brush_size: str | None = None # control_id for brush size
|
|
21
|
+
label_pads: list[str] = Field(default_factory=list) # control_ids for labels
|
|
22
|
+
|
|
23
|
+
# Navigation button mappings
|
|
24
|
+
slice_up: str | None = None # +1 slice step
|
|
25
|
+
slice_down: str | None = None # -1 slice step
|
|
26
|
+
roll_left: str | None = None # Roll dims left
|
|
27
|
+
roll_right: str | None = None # Roll dims right
|
|
28
|
+
|
|
29
|
+
# Transport button mappings
|
|
30
|
+
undo: str | None = None # Undo action (stop button)
|
|
31
|
+
redo: str | None = None # Redo action (play button)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ControlMapper:
|
|
35
|
+
"""Discovers and maps controller controls to napari features.
|
|
36
|
+
|
|
37
|
+
Automatically assigns controls based on their type and capabilities:
|
|
38
|
+
- Faders preferred for coarse slice control
|
|
39
|
+
- Knobs/encoders for fine slice, brush size, zoom
|
|
40
|
+
- Pads for label selection
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, controller: Controller) -> None:
|
|
44
|
+
"""Initialize the control mapper.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
controller: The padbound Controller instance.
|
|
48
|
+
"""
|
|
49
|
+
self.controller = controller
|
|
50
|
+
self.controls: list[ControlDefinition] = controller.get_controls()
|
|
51
|
+
|
|
52
|
+
def create_mapping(self) -> ControlMapping:
|
|
53
|
+
"""Auto-discover and map controls based on capabilities.
|
|
54
|
+
|
|
55
|
+
Priority for continuous controls: fader > knob > encoder
|
|
56
|
+
All mapped controls come from the same bank (for multi-bank controllers).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
ControlMapping with assigned control IDs.
|
|
60
|
+
"""
|
|
61
|
+
mapping = ControlMapping()
|
|
62
|
+
|
|
63
|
+
# Group controls by category
|
|
64
|
+
faders = [c for c in self.controls if c.category == "fader"]
|
|
65
|
+
knobs = [c for c in self.controls if c.category == "knob"]
|
|
66
|
+
encoders = [c for c in self.controls if c.category == "encoder"]
|
|
67
|
+
pads = [c for c in self.controls if c.category == "pad"]
|
|
68
|
+
|
|
69
|
+
# Determine primary bank from first fader (or first continuous control)
|
|
70
|
+
all_continuous = faders + knobs + encoders
|
|
71
|
+
primary_bank = all_continuous[0].bank_id if all_continuous else None
|
|
72
|
+
|
|
73
|
+
# Filter to primary bank only (None matches None for bankless controllers)
|
|
74
|
+
faders = [c for c in faders if c.bank_id == primary_bank]
|
|
75
|
+
knobs = [c for c in knobs if c.bank_id == primary_bank]
|
|
76
|
+
encoders = [c for c in encoders if c.bank_id == primary_bank]
|
|
77
|
+
pads = [c for c in pads if c.bank_id == primary_bank]
|
|
78
|
+
|
|
79
|
+
# Assign continuous controls (priority: fader > knob > encoder)
|
|
80
|
+
continuous = faders + knobs + encoders
|
|
81
|
+
if len(continuous) >= 1:
|
|
82
|
+
mapping.coarse_slice = continuous[0].control_id
|
|
83
|
+
if len(continuous) >= 2:
|
|
84
|
+
mapping.fine_slice = continuous[1].control_id
|
|
85
|
+
if len(continuous) >= 3:
|
|
86
|
+
mapping.brush_size = continuous[2].control_id
|
|
87
|
+
if len(continuous) >= 4:
|
|
88
|
+
mapping.zoom = continuous[3].control_id
|
|
89
|
+
|
|
90
|
+
# Assign pads for label selection
|
|
91
|
+
mapping.label_pads = [p.control_id for p in pads]
|
|
92
|
+
|
|
93
|
+
# Discover navigation buttons (for slice stepping and dim rolling)
|
|
94
|
+
nav_controls = [
|
|
95
|
+
c for c in self.controls
|
|
96
|
+
if c.category == "navigation" and c.bank_id == primary_bank
|
|
97
|
+
]
|
|
98
|
+
for c in nav_controls:
|
|
99
|
+
cid = c.control_id.lower()
|
|
100
|
+
if cid in ("up", "nav_up") and mapping.slice_up is None:
|
|
101
|
+
mapping.slice_up = c.control_id
|
|
102
|
+
elif cid in ("down", "nav_down") and mapping.slice_down is None:
|
|
103
|
+
mapping.slice_down = c.control_id
|
|
104
|
+
elif cid in ("left", "nav_left") and mapping.roll_left is None:
|
|
105
|
+
mapping.roll_left = c.control_id
|
|
106
|
+
elif cid in ("right", "nav_right") and mapping.roll_right is None:
|
|
107
|
+
mapping.roll_right = c.control_id
|
|
108
|
+
|
|
109
|
+
# Discover transport buttons (for undo/redo)
|
|
110
|
+
transport_controls = [
|
|
111
|
+
c for c in self.controls
|
|
112
|
+
if c.category == "transport" and c.bank_id == primary_bank
|
|
113
|
+
]
|
|
114
|
+
for c in transport_controls:
|
|
115
|
+
cid = c.control_id.lower()
|
|
116
|
+
if cid == "stop" and mapping.undo is None:
|
|
117
|
+
mapping.undo = c.control_id
|
|
118
|
+
elif cid == "play" and mapping.redo is None:
|
|
119
|
+
mapping.redo = c.control_id
|
|
120
|
+
|
|
121
|
+
return mapping
|
|
122
|
+
|
|
123
|
+
def get_mapping_info(self) -> str:
|
|
124
|
+
"""Get human-readable description of the control mapping.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Multi-line string describing the mapping.
|
|
128
|
+
"""
|
|
129
|
+
mapping = self.create_mapping()
|
|
130
|
+
lines = []
|
|
131
|
+
|
|
132
|
+
if mapping.coarse_slice:
|
|
133
|
+
lines.append(f"Coarse slice: {mapping.coarse_slice}")
|
|
134
|
+
if mapping.fine_slice:
|
|
135
|
+
lines.append(f"Fine slice: {mapping.fine_slice}")
|
|
136
|
+
if mapping.brush_size:
|
|
137
|
+
lines.append(f"Brush size: {mapping.brush_size}")
|
|
138
|
+
if mapping.zoom:
|
|
139
|
+
lines.append(f"Zoom: {mapping.zoom}")
|
|
140
|
+
if mapping.label_pads:
|
|
141
|
+
lines.append(f"Label pads: {len(mapping.label_pads)} pads")
|
|
142
|
+
if mapping.slice_up:
|
|
143
|
+
lines.append(f"Slice up: {mapping.slice_up}")
|
|
144
|
+
if mapping.slice_down:
|
|
145
|
+
lines.append(f"Slice down: {mapping.slice_down}")
|
|
146
|
+
if mapping.roll_left:
|
|
147
|
+
lines.append(f"Roll left: {mapping.roll_left}")
|
|
148
|
+
if mapping.roll_right:
|
|
149
|
+
lines.append(f"Roll right: {mapping.roll_right}")
|
|
150
|
+
if mapping.undo:
|
|
151
|
+
lines.append(f"Undo: {mapping.undo}")
|
|
152
|
+
if mapping.redo:
|
|
153
|
+
lines.append(f"Redo: {mapping.redo}")
|
|
154
|
+
|
|
155
|
+
return "\n".join(lines) if lines else "No controls mapped"
|