py2pd 0.2.0__tar.gz → 0.2.2__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.
- {py2pd-0.2.0 → py2pd-0.2.2}/PKG-INFO +1 -1
- {py2pd-0.2.0 → py2pd-0.2.2}/pyproject.toml +1 -1
- {py2pd-0.2.0 → py2pd-0.2.2}/pyproject.toml.orig +1 -1
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/__init__.py +94 -92
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/api.py +147 -36
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/ast.py +7 -6
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/integrations/hvcc.py +3 -2
- {py2pd-0.2.0 → py2pd-0.2.2}/LICENSE +0 -0
- {py2pd-0.2.0 → py2pd-0.2.2}/README.md +0 -0
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/discover.py +0 -0
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/integrations/__init__.py +0 -0
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/integrations/cypd.py +0 -0
- {py2pd-0.2.0 → py2pd-0.2.2}/src/py2pd/py.typed +0 -0
|
@@ -1,92 +1,94 @@
|
|
|
1
|
-
"""
|
|
2
|
-
py2pd - Python to PureData
|
|
3
|
-
==========================
|
|
4
|
-
|
|
5
|
-
Write PureData patches as Python programs.
|
|
6
|
-
|
|
7
|
-
Builder API example:
|
|
8
|
-
>>> from py2pd import Patcher
|
|
9
|
-
>>> p = Patcher('patch.pd')
|
|
10
|
-
>>> osc = p.add('osc~ 440')
|
|
11
|
-
>>> dac = p.add('dac~')
|
|
12
|
-
>>> p.link(osc, dac)
|
|
13
|
-
>>> p.link(osc, dac, inlet=1) # stereo
|
|
14
|
-
>>> p.save()
|
|
15
|
-
|
|
16
|
-
AST API example (round-trip):
|
|
17
|
-
>>> from py2pd import parse_file, serialize
|
|
18
|
-
>>> ast = parse_file('input.pd')
|
|
19
|
-
>>> # Modify the AST...
|
|
20
|
-
>>> with open('output.pd', 'w') as f:
|
|
21
|
-
... f.write(serialize(ast))
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from .api import (
|
|
25
|
-
COLUMN_WIDTH,
|
|
26
|
-
DEFAULT_MARGIN,
|
|
27
|
-
ROW_HEIGHT,
|
|
28
|
-
SUBPATCH_CANVAS_HEIGHT,
|
|
29
|
-
SUBPATCH_CANVAS_WIDTH,
|
|
30
|
-
Abstraction,
|
|
31
|
-
CycleWarning,
|
|
32
|
-
GridLayoutManager,
|
|
33
|
-
InvalidConnectionError,
|
|
34
|
-
LayoutManager,
|
|
35
|
-
NodeNotFoundError,
|
|
36
|
-
Patcher,
|
|
37
|
-
PdConnectionError,
|
|
38
|
-
PdConnectionWarning,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
#
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
"""
|
|
2
|
+
py2pd - Python to PureData
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
Write PureData patches as Python programs.
|
|
6
|
+
|
|
7
|
+
Builder API example:
|
|
8
|
+
>>> from py2pd import Patcher
|
|
9
|
+
>>> p = Patcher('patch.pd')
|
|
10
|
+
>>> osc = p.add('osc~ 440')
|
|
11
|
+
>>> dac = p.add('dac~')
|
|
12
|
+
>>> p.link(osc, dac)
|
|
13
|
+
>>> p.link(osc, dac, inlet=1) # stereo
|
|
14
|
+
>>> p.save()
|
|
15
|
+
|
|
16
|
+
AST API example (round-trip):
|
|
17
|
+
>>> from py2pd import parse_file, serialize
|
|
18
|
+
>>> ast = parse_file('input.pd')
|
|
19
|
+
>>> # Modify the AST...
|
|
20
|
+
>>> with open('output.pd', 'w') as f:
|
|
21
|
+
... f.write(serialize(ast))
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .api import (
|
|
25
|
+
COLUMN_WIDTH,
|
|
26
|
+
DEFAULT_MARGIN,
|
|
27
|
+
ROW_HEIGHT,
|
|
28
|
+
SUBPATCH_CANVAS_HEIGHT,
|
|
29
|
+
SUBPATCH_CANVAS_WIDTH,
|
|
30
|
+
Abstraction,
|
|
31
|
+
CycleWarning,
|
|
32
|
+
GridLayoutManager,
|
|
33
|
+
InvalidConnectionError,
|
|
34
|
+
LayoutManager,
|
|
35
|
+
NodeNotFoundError,
|
|
36
|
+
Patcher,
|
|
37
|
+
PdConnectionError,
|
|
38
|
+
PdConnectionWarning,
|
|
39
|
+
SubpatchIOOrderWarning,
|
|
40
|
+
lookup_object_io,
|
|
41
|
+
)
|
|
42
|
+
from .ast import (
|
|
43
|
+
ParseError,
|
|
44
|
+
UnsupportedElementWarning,
|
|
45
|
+
from_builder,
|
|
46
|
+
parse,
|
|
47
|
+
parse_file,
|
|
48
|
+
serialize,
|
|
49
|
+
serialize_to_file,
|
|
50
|
+
to_builder,
|
|
51
|
+
)
|
|
52
|
+
from .discover import (
|
|
53
|
+
default_search_paths,
|
|
54
|
+
discover_externals,
|
|
55
|
+
extract_declare_paths,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Builder API
|
|
60
|
+
"Patcher",
|
|
61
|
+
"Abstraction",
|
|
62
|
+
"LayoutManager",
|
|
63
|
+
"GridLayoutManager",
|
|
64
|
+
# Exceptions
|
|
65
|
+
"PdConnectionError",
|
|
66
|
+
"NodeNotFoundError",
|
|
67
|
+
"InvalidConnectionError",
|
|
68
|
+
"CycleWarning",
|
|
69
|
+
"PdConnectionWarning",
|
|
70
|
+
"SubpatchIOOrderWarning",
|
|
71
|
+
"UnsupportedElementWarning",
|
|
72
|
+
"ParseError",
|
|
73
|
+
# Object registry
|
|
74
|
+
"lookup_object_io",
|
|
75
|
+
# Layout constants
|
|
76
|
+
"ROW_HEIGHT",
|
|
77
|
+
"COLUMN_WIDTH",
|
|
78
|
+
"DEFAULT_MARGIN",
|
|
79
|
+
"SUBPATCH_CANVAS_WIDTH",
|
|
80
|
+
"SUBPATCH_CANVAS_HEIGHT",
|
|
81
|
+
# AST functions
|
|
82
|
+
"parse",
|
|
83
|
+
"parse_file",
|
|
84
|
+
"serialize",
|
|
85
|
+
"serialize_to_file",
|
|
86
|
+
"from_builder",
|
|
87
|
+
"to_builder",
|
|
88
|
+
# Discovery
|
|
89
|
+
"discover_externals",
|
|
90
|
+
"default_search_paths",
|
|
91
|
+
"extract_declare_paths",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
__version__ = "0.2.2"
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
from collections import deque
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
import re
|
|
3
4
|
from typing import Any, Callable, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union
|
|
4
5
|
import warnings
|
|
5
6
|
|
|
7
|
+
# The builder writes the same number format as the AST serializer, so it uses
|
|
8
|
+
# the same helper. ast imports api only lazily, so this direction does not cycle.
|
|
9
|
+
from .ast import _fmt_num
|
|
10
|
+
|
|
6
11
|
# Layout constants (pixels)
|
|
7
12
|
ROW_HEIGHT = 25
|
|
8
13
|
COLUMN_WIDTH = 50
|
|
@@ -52,6 +57,12 @@ class CycleWarning(UserWarning):
|
|
|
52
57
|
pass
|
|
53
58
|
|
|
54
59
|
|
|
60
|
+
class SubpatchIOOrderWarning(UserWarning):
|
|
61
|
+
"""A subpatch's inlet or outlet order will not match creation order."""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
55
66
|
class PdConnectionWarning(UserWarning):
|
|
56
67
|
"""Warning raised for an out-of-range connection when validation is advisory.
|
|
57
68
|
|
|
@@ -63,18 +74,74 @@ class PdConnectionWarning(UserWarning):
|
|
|
63
74
|
pass
|
|
64
75
|
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
_IO_KINDS = (("inlet", ("inlet", "inlet~")), ("outlet", ("outlet", "outlet~")))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _subpatch_io_nodes(src: "Patcher", names: Tuple[str, str]) -> List["Obj"]:
|
|
81
|
+
"""The inlet or outlet objects of *src*, in creation order."""
|
|
82
|
+
return [
|
|
83
|
+
n
|
|
84
|
+
for n in src.nodes
|
|
85
|
+
if isinstance(n, Obj) and n.parameters["text"].split()[:1] in ([names[0]], [names[1]])
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _order_subpatch_io(src: "Patcher") -> None:
|
|
90
|
+
"""Spread a subpatch's inlet and outlet objects so PureData orders them by creation.
|
|
91
|
+
|
|
92
|
+
PureData derives a subpatch's inlet and outlet order from the x position of
|
|
93
|
+
the ``inlet`` / ``outlet`` objects, breaking a tie in reverse file order.
|
|
94
|
+
The default layout stacks every node at one x, so two inlets created in
|
|
95
|
+
order reach the parent reversed -- a patch that loads without complaint and
|
|
96
|
+
is wired backwards.
|
|
97
|
+
|
|
98
|
+
Only a tie is repaired, and only among objects the caller never positioned:
|
|
99
|
+
distinct positions already give an unambiguous order, and an explicit
|
|
100
|
+
``x_pos`` is a decision to respect. Repositioning is by creation order, so
|
|
101
|
+
``link(..., inlet=n)`` means the n-th ``inlet`` object created.
|
|
102
|
+
"""
|
|
103
|
+
for _, names in _IO_KINDS:
|
|
104
|
+
nodes = _subpatch_io_nodes(src, names)
|
|
105
|
+
if len(nodes) < 2:
|
|
106
|
+
continue
|
|
107
|
+
xs = [n.parameters["x_pos"] for n in nodes]
|
|
108
|
+
if len(set(xs)) == len(xs):
|
|
109
|
+
continue # already unambiguous
|
|
110
|
+
if any(id(n) in src._explicit_x for n in nodes):
|
|
111
|
+
continue # the caller placed at least one; leave the layout alone
|
|
112
|
+
base = min(xs)
|
|
113
|
+
for i, node in enumerate(nodes):
|
|
114
|
+
node.parameters["x_pos"] = base + i * SUBPATCH_IO_SPACING
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _warn_on_ambiguous_subpatch_io(src: "Patcher", name: str) -> None:
|
|
118
|
+
"""Warn when a subpatch's inlet or outlet order will not match creation order.
|
|
119
|
+
|
|
120
|
+
PureData orders a subpatch's inlets and outlets by the x position of the
|
|
121
|
+
``inlet`` / ``outlet`` objects inside it, and breaks a tie in reverse file
|
|
122
|
+
order. The default layout stacks every node at the same x, so two inlets
|
|
123
|
+
created in order arrive at the parent reversed -- a patch that loads
|
|
124
|
+
without complaint and is wired wrong.
|
|
125
|
+
"""
|
|
126
|
+
for kind, names in _IO_KINDS:
|
|
127
|
+
xs = [n.parameters["x_pos"] for n in _subpatch_io_nodes(src, names)]
|
|
128
|
+
if len(xs) > 1 and len(set(xs)) != len(xs):
|
|
129
|
+
warnings.warn(
|
|
130
|
+
f"subpatch {name!r} has {len(xs)} {kind}s sharing an x position; "
|
|
131
|
+
f"PureData orders them by x and will not use creation order. "
|
|
132
|
+
f"Give each {kind} a distinct x_pos to fix the order.",
|
|
133
|
+
SubpatchIOOrderWarning,
|
|
134
|
+
stacklevel=3,
|
|
135
|
+
)
|
|
71
136
|
|
|
72
137
|
|
|
73
138
|
def escape(text: str) -> str:
|
|
74
139
|
"""Escape special characters for PureData format."""
|
|
75
140
|
save = re.sub(r"\\", r"\\\\", text)
|
|
76
|
-
|
|
77
|
-
|
|
141
|
+
# Absorb whitespace already flanking the separator: PureData puts one space
|
|
142
|
+
# between atoms, and "0, 1 10" would otherwise gain a second one.
|
|
143
|
+
save = re.sub(r"[ \t]*;[ \t]*", r" \; ", save)
|
|
144
|
+
save = re.sub(r"[ \t]*,[ \t]*", r" \, ", save)
|
|
78
145
|
save = re.sub(r"\$(?=[0-9])", r"\$", save)
|
|
79
146
|
return save
|
|
80
147
|
|
|
@@ -310,7 +377,8 @@ class Msg(Node):
|
|
|
310
377
|
text : str
|
|
311
378
|
Message content. Will be escaped for the PureData file format.
|
|
312
379
|
num_inlets : int, optional
|
|
313
|
-
Number of inlets (default:
|
|
380
|
+
Number of inlets (default: 1). A message box has one inlet; contents
|
|
381
|
+
are replaced by sending ``set ...`` to it, not through a second inlet.
|
|
314
382
|
num_outlets : int, optional
|
|
315
383
|
Number of outlets (default: 1)
|
|
316
384
|
escaped : bool, optional
|
|
@@ -323,7 +391,7 @@ class Msg(Node):
|
|
|
323
391
|
x_pos: int,
|
|
324
392
|
y_pos: int,
|
|
325
393
|
text: str,
|
|
326
|
-
num_inlets: Optional[int] =
|
|
394
|
+
num_inlets: Optional[int] = 1,
|
|
327
395
|
num_outlets: Optional[int] = 1,
|
|
328
396
|
*,
|
|
329
397
|
escaped: bool = False,
|
|
@@ -368,10 +436,12 @@ class Float(Node):
|
|
|
368
436
|
Y position in patch coordinates
|
|
369
437
|
width : int
|
|
370
438
|
Display width in characters (default: 5)
|
|
371
|
-
upper_limit : float
|
|
372
|
-
Maximum value; 0 means no limit (default: 0)
|
|
373
439
|
lower_limit : float
|
|
374
440
|
Minimum value; 0 means no limit (default: 0)
|
|
441
|
+
upper_limit : float
|
|
442
|
+
Maximum value; 0 means no limit (default: 0)
|
|
443
|
+
label_pos : int
|
|
444
|
+
Label placement: 0=left, 1=right, 2=top, 3=bottom (default: 0)
|
|
375
445
|
label : str
|
|
376
446
|
Label text (default: ``'-'`` for none)
|
|
377
447
|
receive : str
|
|
@@ -385,8 +455,9 @@ class Float(Node):
|
|
|
385
455
|
x_pos: int,
|
|
386
456
|
y_pos: int,
|
|
387
457
|
width: int = 5,
|
|
388
|
-
upper_limit: float = 0,
|
|
389
458
|
lower_limit: float = 0,
|
|
459
|
+
upper_limit: float = 0,
|
|
460
|
+
label_pos: int = 0,
|
|
390
461
|
label: str = "-",
|
|
391
462
|
receive: str = "-",
|
|
392
463
|
send: str = "-",
|
|
@@ -397,8 +468,9 @@ class Float(Node):
|
|
|
397
468
|
"x_pos": x_pos,
|
|
398
469
|
"y_pos": y_pos,
|
|
399
470
|
"width": width,
|
|
400
|
-
"upper_limit": upper_limit,
|
|
401
471
|
"lower_limit": lower_limit,
|
|
472
|
+
"upper_limit": upper_limit,
|
|
473
|
+
"label_pos": label_pos,
|
|
402
474
|
"label": label,
|
|
403
475
|
"receive": receive,
|
|
404
476
|
"send": send,
|
|
@@ -410,8 +482,8 @@ class Float(Node):
|
|
|
410
482
|
p = self.parameters
|
|
411
483
|
return (
|
|
412
484
|
f"#X floatatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
413
|
-
f"{p['
|
|
414
|
-
f"{p['receive']} {p['send']};\n"
|
|
485
|
+
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
486
|
+
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
415
487
|
)
|
|
416
488
|
|
|
417
489
|
@property
|
|
@@ -461,6 +533,9 @@ class Comment(Node):
|
|
|
461
533
|
|
|
462
534
|
|
|
463
535
|
# Default subpatch canvas dimensions (pixels)
|
|
536
|
+
# Horizontal gap applied when spreading a subpatch's inlet/outlet objects so
|
|
537
|
+
# PureData orders them by creation rather than by an arbitrary tie-break.
|
|
538
|
+
SUBPATCH_IO_SPACING = 70
|
|
464
539
|
SUBPATCH_CANVAS_WIDTH = 300
|
|
465
540
|
SUBPATCH_CANVAS_HEIGHT = 180
|
|
466
541
|
|
|
@@ -605,7 +680,7 @@ class Subpatch(Node):
|
|
|
605
680
|
# separate field, and the two values after the flag are the
|
|
606
681
|
# viewport margins.
|
|
607
682
|
gop_flag = 2 if p["hide_name"] else 1
|
|
608
|
-
rect = " ".join(
|
|
683
|
+
rect = " ".join(_fmt_num(v) for v in p.get("gop_rect", (0, 1, 1, 0)))
|
|
609
684
|
margins = p.get("gop_margins", (0, 0))
|
|
610
685
|
tail = "" if margins is None else f" {margins[0]} {margins[1]}"
|
|
611
686
|
coords_line = f"#X coords {rect} {p['gop_width']} {p['gop_height']} {gop_flag}{tail};\n"
|
|
@@ -987,7 +1062,7 @@ class Symbol(Node):
|
|
|
987
1062
|
p = self.parameters
|
|
988
1063
|
return (
|
|
989
1064
|
f"#X symbolatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
990
|
-
f"{p['lower_limit']} {p['upper_limit']} {p['label_pos']} "
|
|
1065
|
+
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
991
1066
|
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
992
1067
|
)
|
|
993
1068
|
|
|
@@ -1065,11 +1140,11 @@ class NumberBox(Node):
|
|
|
1065
1140
|
p = self.parameters
|
|
1066
1141
|
return (
|
|
1067
1142
|
f"#X obj {p['x_pos']} {p['y_pos']} nbx {p['width']} {p['height']} "
|
|
1068
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1143
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1069
1144
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1070
1145
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1071
1146
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1072
|
-
f"{p['init_value']} {p['log_height']};\n"
|
|
1147
|
+
f"{_fmt_num(p['init_value'])} {p['log_height']};\n"
|
|
1073
1148
|
)
|
|
1074
1149
|
|
|
1075
1150
|
@property
|
|
@@ -1143,11 +1218,11 @@ class VSlider(Node):
|
|
|
1143
1218
|
p = self.parameters
|
|
1144
1219
|
return (
|
|
1145
1220
|
f"#X obj {p['x_pos']} {p['y_pos']} vsl {p['width']} {p['height']} "
|
|
1146
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1221
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1147
1222
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1148
1223
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1149
1224
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1150
|
-
f"{p['init_value']} {p['steady']};\n"
|
|
1225
|
+
f"{_fmt_num(p['init_value'])} {p['steady']};\n"
|
|
1151
1226
|
)
|
|
1152
1227
|
|
|
1153
1228
|
@property
|
|
@@ -1218,11 +1293,11 @@ class HSlider(Node):
|
|
|
1218
1293
|
p = self.parameters
|
|
1219
1294
|
return (
|
|
1220
1295
|
f"#X obj {p['x_pos']} {p['y_pos']} hsl {p['width']} {p['height']} "
|
|
1221
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1296
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1222
1297
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1223
1298
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1224
1299
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1225
|
-
f"{p['init_value']} {p['steady']};\n"
|
|
1300
|
+
f"{_fmt_num(p['init_value'])} {p['steady']};\n"
|
|
1226
1301
|
)
|
|
1227
1302
|
|
|
1228
1303
|
@property
|
|
@@ -2143,7 +2218,7 @@ class Patcher:
|
|
|
2143
2218
|
|
|
2144
2219
|
Attributes
|
|
2145
2220
|
----------
|
|
2146
|
-
filename :
|
|
2221
|
+
filename : Path or None
|
|
2147
2222
|
Default filename for save()
|
|
2148
2223
|
nodes : list of Node
|
|
2149
2224
|
All nodes in the patch
|
|
@@ -2153,7 +2228,7 @@ class Patcher:
|
|
|
2153
2228
|
Manages automatic element positioning
|
|
2154
2229
|
"""
|
|
2155
2230
|
|
|
2156
|
-
filename: Optional[
|
|
2231
|
+
filename: Optional[Path]
|
|
2157
2232
|
nodes: List[Node]
|
|
2158
2233
|
connections: List[Connection]
|
|
2159
2234
|
layout: LayoutManager
|
|
@@ -2161,7 +2236,7 @@ class Patcher:
|
|
|
2161
2236
|
|
|
2162
2237
|
def __init__(
|
|
2163
2238
|
self,
|
|
2164
|
-
filename: Optional[str] = None,
|
|
2239
|
+
filename: Optional[Union[str, Path]] = None,
|
|
2165
2240
|
layout: Optional[LayoutManager] = None,
|
|
2166
2241
|
*,
|
|
2167
2242
|
validate_links: bool = True,
|
|
@@ -2175,8 +2250,9 @@ class Patcher:
|
|
|
2175
2250
|
|
|
2176
2251
|
Parameters
|
|
2177
2252
|
----------
|
|
2178
|
-
filename : str, optional
|
|
2179
|
-
Default filename for save()
|
|
2253
|
+
filename : str or Path, optional
|
|
2254
|
+
Default filename for save(), stored as a ``Path``. Can be
|
|
2255
|
+
overridden in save().
|
|
2180
2256
|
layout : LayoutManager, optional
|
|
2181
2257
|
Custom layout manager. If None, creates a default LayoutManager.
|
|
2182
2258
|
validate_links : bool, optional
|
|
@@ -2193,7 +2269,7 @@ class Patcher:
|
|
|
2193
2269
|
font_size : int, optional
|
|
2194
2270
|
Patch font size written on the ``#N canvas`` line (default: 10).
|
|
2195
2271
|
"""
|
|
2196
|
-
self.filename = filename
|
|
2272
|
+
self.filename = Path(filename) if filename is not None else None
|
|
2197
2273
|
self.nodes = []
|
|
2198
2274
|
self.connections = []
|
|
2199
2275
|
self.layout = layout if layout is not None else LayoutManager()
|
|
@@ -2204,6 +2280,9 @@ class Patcher:
|
|
|
2204
2280
|
self.canvas_height = canvas_height
|
|
2205
2281
|
self.font_size = font_size
|
|
2206
2282
|
self._node_positions: Dict[int, int] = {}
|
|
2283
|
+
# Nodes whose x the caller set. Subpatch I/O ordering may reposition
|
|
2284
|
+
# the others; it must never move one the caller placed.
|
|
2285
|
+
self._explicit_x: Set[int] = set()
|
|
2207
2286
|
|
|
2208
2287
|
@property
|
|
2209
2288
|
def row_head(self) -> Optional[Node]:
|
|
@@ -2249,10 +2328,13 @@ class Patcher:
|
|
|
2249
2328
|
) -> Tuple[int, int, Callable[[Node], None]]:
|
|
2250
2329
|
"""Resolve position for a new element."""
|
|
2251
2330
|
was_absolute = x_pos >= 0 and y_pos >= 0
|
|
2331
|
+
x_was_given = x_pos >= 0
|
|
2252
2332
|
computed_x, computed_y = self.layout.compute_position(new_row, new_col, x_pos, y_pos)
|
|
2253
2333
|
|
|
2254
2334
|
def position_update(node: Node) -> None:
|
|
2255
2335
|
self.layout.register_node(node, new_row, new_col, was_absolute)
|
|
2336
|
+
if x_was_given:
|
|
2337
|
+
self._explicit_x.add(id(node))
|
|
2256
2338
|
|
|
2257
2339
|
return (computed_x, computed_y, position_update)
|
|
2258
2340
|
|
|
@@ -2412,8 +2494,9 @@ class Patcher:
|
|
|
2412
2494
|
self,
|
|
2413
2495
|
*,
|
|
2414
2496
|
width: int = 5,
|
|
2415
|
-
upper_limit: float = 0,
|
|
2416
2497
|
lower_limit: float = 0,
|
|
2498
|
+
upper_limit: float = 0,
|
|
2499
|
+
label_pos: int = 0,
|
|
2417
2500
|
label: str = "-",
|
|
2418
2501
|
receive: str = "-",
|
|
2419
2502
|
send: str = "-",
|
|
@@ -2428,10 +2511,12 @@ class Patcher:
|
|
|
2428
2511
|
----------
|
|
2429
2512
|
width : int
|
|
2430
2513
|
Display width in characters (default: 5)
|
|
2431
|
-
upper_limit : int
|
|
2432
|
-
Maximum value (0 = no limit)
|
|
2433
2514
|
lower_limit : int
|
|
2434
2515
|
Minimum value (0 = no limit)
|
|
2516
|
+
upper_limit : int
|
|
2517
|
+
Maximum value (0 = no limit)
|
|
2518
|
+
label_pos : int
|
|
2519
|
+
Label placement: 0=left, 1=right, 2=top, 3=bottom
|
|
2435
2520
|
label : str
|
|
2436
2521
|
Label text (default: '-' for none)
|
|
2437
2522
|
receive : str
|
|
@@ -2445,7 +2530,17 @@ class Patcher:
|
|
|
2445
2530
|
The created number box
|
|
2446
2531
|
"""
|
|
2447
2532
|
x_pos, y_pos, pos_update = self._resolve_position(x_pos, y_pos, new_row, new_col)
|
|
2448
|
-
node = Float(
|
|
2533
|
+
node = Float(
|
|
2534
|
+
x_pos,
|
|
2535
|
+
y_pos,
|
|
2536
|
+
width=width,
|
|
2537
|
+
lower_limit=lower_limit,
|
|
2538
|
+
upper_limit=upper_limit,
|
|
2539
|
+
label_pos=label_pos,
|
|
2540
|
+
label=label,
|
|
2541
|
+
receive=receive,
|
|
2542
|
+
send=send,
|
|
2543
|
+
)
|
|
2449
2544
|
self._register(node, pos_update)
|
|
2450
2545
|
return node
|
|
2451
2546
|
|
|
@@ -2554,6 +2649,9 @@ class Patcher:
|
|
|
2554
2649
|
and n.parameters["text"].split()[:1] in (["outlet"], ["outlet~"])
|
|
2555
2650
|
)
|
|
2556
2651
|
|
|
2652
|
+
_order_subpatch_io(src)
|
|
2653
|
+
_warn_on_ambiguous_subpatch_io(src, name)
|
|
2654
|
+
|
|
2557
2655
|
x_pos, y_pos, pos_update = self._resolve_position(x_pos, y_pos, new_row, new_col)
|
|
2558
2656
|
node = Subpatch(
|
|
2559
2657
|
x_pos,
|
|
@@ -3408,23 +3506,36 @@ class Patcher:
|
|
|
3408
3506
|
connections_str = "".join(str(c) for c in self.connections)
|
|
3409
3507
|
return f"{nodes_str}{connections_str}"
|
|
3410
3508
|
|
|
3411
|
-
def save(
|
|
3509
|
+
def save(
|
|
3510
|
+
self,
|
|
3511
|
+
filename: Optional[Union[str, Path]] = None,
|
|
3512
|
+
*,
|
|
3513
|
+
to_dir: Optional[Union[str, Path]] = None,
|
|
3514
|
+
) -> None:
|
|
3412
3515
|
"""Save the patch to a file.
|
|
3413
3516
|
|
|
3414
3517
|
Parameters
|
|
3415
3518
|
----------
|
|
3416
|
-
filename : str, optional
|
|
3519
|
+
filename : str or Path, optional
|
|
3417
3520
|
Path to the output .pd file. If not provided, uses the filename
|
|
3418
3521
|
from the constructor.
|
|
3522
|
+
to_dir : str or Path, optional
|
|
3523
|
+
Directory to write into, created if missing. Only the base name of
|
|
3524
|
+
*filename* is used, so one patch can be written to several
|
|
3525
|
+
directories.
|
|
3419
3526
|
|
|
3420
3527
|
Raises
|
|
3421
3528
|
------
|
|
3422
3529
|
ValueError
|
|
3423
3530
|
If no filename is provided and none was set in constructor.
|
|
3424
3531
|
"""
|
|
3425
|
-
fn = filename
|
|
3532
|
+
fn = Path(filename) if filename is not None else self.filename
|
|
3426
3533
|
if fn is None:
|
|
3427
3534
|
raise ValueError("No filename specified. Provide filename or set in constructor.")
|
|
3535
|
+
if to_dir is not None:
|
|
3536
|
+
d = Path(to_dir)
|
|
3537
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
3538
|
+
fn = d / fn.name
|
|
3428
3539
|
with open(fn, "w", encoding="utf-8", newline="\n") as f:
|
|
3429
3540
|
f.write(str(self) + "\n")
|
|
3430
3541
|
|
|
@@ -1874,12 +1874,13 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1874
1874
|
node = api.Float(
|
|
1875
1875
|
elem.position.x,
|
|
1876
1876
|
elem.position.y,
|
|
1877
|
-
elem.width,
|
|
1878
|
-
elem.
|
|
1879
|
-
elem.
|
|
1880
|
-
elem.
|
|
1881
|
-
elem.
|
|
1882
|
-
elem.
|
|
1877
|
+
width=elem.width,
|
|
1878
|
+
lower_limit=elem.lower_limit,
|
|
1879
|
+
upper_limit=elem.upper_limit,
|
|
1880
|
+
label_pos=elem.label_pos,
|
|
1881
|
+
label=elem.label,
|
|
1882
|
+
receive=elem.receive,
|
|
1883
|
+
send=elem.send,
|
|
1883
1884
|
)
|
|
1884
1885
|
patch.nodes.append(node)
|
|
1885
1886
|
node_map.append(node)
|
|
@@ -28,6 +28,7 @@ Usage::
|
|
|
28
28
|
from dataclasses import dataclass, field
|
|
29
29
|
from enum import Enum
|
|
30
30
|
import os
|
|
31
|
+
from pathlib import Path
|
|
31
32
|
import re
|
|
32
33
|
import shutil
|
|
33
34
|
import subprocess
|
|
@@ -425,7 +426,7 @@ class HeavyPatcher(Patcher):
|
|
|
425
426
|
|
|
426
427
|
Parameters
|
|
427
428
|
----------
|
|
428
|
-
filename : str, optional
|
|
429
|
+
filename : str or Path, optional
|
|
429
430
|
Default filename for save().
|
|
430
431
|
layout : LayoutManager, optional
|
|
431
432
|
Custom layout manager.
|
|
@@ -435,7 +436,7 @@ class HeavyPatcher(Patcher):
|
|
|
435
436
|
|
|
436
437
|
def __init__(
|
|
437
438
|
self,
|
|
438
|
-
filename: Optional[str] = None,
|
|
439
|
+
filename: Optional[Union[str, Path]] = None,
|
|
439
440
|
layout: Optional[LayoutManager] = None,
|
|
440
441
|
generators: Sequence[HvccGenerator] | None = None,
|
|
441
442
|
) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|