py2pd 0.2.1__tar.gz → 0.2.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.
- {py2pd-0.2.1 → py2pd-0.2.3}/PKG-INFO +2 -2
- {py2pd-0.2.1 → py2pd-0.2.3}/README.md +1 -1
- {py2pd-0.2.1 → py2pd-0.2.3}/pyproject.toml +1 -1
- {py2pd-0.2.1 → py2pd-0.2.3}/pyproject.toml.orig +1 -1
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/__init__.py +94 -92
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/api.py +319 -32
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/ast.py +71 -37
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/discover.py +5 -2
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/integrations/cypd.py +4 -4
- {py2pd-0.2.1 → py2pd-0.2.3}/LICENSE +0 -0
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/integrations/__init__.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/integrations/hvcc.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.3}/src/py2pd/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2pd
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Roundtrip parsing and generation of pure-data patches from python
|
|
5
5
|
Keywords: puredata,pd,audio,dsp,music,synthesis,patching
|
|
6
6
|
Author: Shakeeb Alireza
|
|
@@ -483,4 +483,4 @@ from py2pd import (
|
|
|
483
483
|
|
|
484
484
|
`PdConnectionError` is raised eagerly by `link()` when outlet or inlet indices exceed the node's known I/O counts. `Node.__getitem__` (e.g., `osc[2]`) raises `ValueError` for out-of-range outlet indices. Both checks are skipped for objects with unknown counts (`num_outlets=None` / `num_inlets=None`), and `link()` warns instead of raising when the patch was created with `validate_links=False`.
|
|
485
485
|
|
|
486
|
-
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for
|
|
486
|
+
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for a connection it cannot rebuild, rather than dropping patch content silently. Statements the Builder does not model are carried verbatim instead of warned about, so `parse -> to_builder -> from_builder -> serialize` returns the bytes it started with.
|
|
@@ -451,4 +451,4 @@ from py2pd import (
|
|
|
451
451
|
|
|
452
452
|
`PdConnectionError` is raised eagerly by `link()` when outlet or inlet indices exceed the node's known I/O counts. `Node.__getitem__` (e.g., `osc[2]`) raises `ValueError` for out-of-range outlet indices. Both checks are skipped for objects with unknown counts (`num_outlets=None` / `num_inlets=None`), and `link()` warns instead of raising when the patch was created with `validate_links=False`.
|
|
453
453
|
|
|
454
|
-
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for
|
|
454
|
+
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for a connection it cannot rebuild, rather than dropping patch content silently. Statements the Builder does not model are carried verbatim instead of warned about, so `parse -> to_builder -> from_builder -> serialize` returns the bytes it started with.
|
|
@@ -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.3"
|
|
@@ -4,6 +4,10 @@ import re
|
|
|
4
4
|
from typing import Any, Callable, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union
|
|
5
5
|
import warnings
|
|
6
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 PdCoords, PdDeclare, _fmt_num
|
|
10
|
+
|
|
7
11
|
# Layout constants (pixels)
|
|
8
12
|
ROW_HEIGHT = 25
|
|
9
13
|
COLUMN_WIDTH = 50
|
|
@@ -53,6 +57,12 @@ class CycleWarning(UserWarning):
|
|
|
53
57
|
pass
|
|
54
58
|
|
|
55
59
|
|
|
60
|
+
class SubpatchIOOrderWarning(UserWarning):
|
|
61
|
+
"""A subpatch's inlet or outlet order will not match creation order."""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
56
66
|
class PdConnectionWarning(UserWarning):
|
|
57
67
|
"""Warning raised for an out-of-range connection when validation is advisory.
|
|
58
68
|
|
|
@@ -64,11 +74,65 @@ class PdConnectionWarning(UserWarning):
|
|
|
64
74
|
pass
|
|
65
75
|
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
)
|
|
72
136
|
|
|
73
137
|
|
|
74
138
|
def escape(text: str) -> str:
|
|
@@ -157,6 +221,20 @@ class Node:
|
|
|
157
221
|
hidden: bool = False
|
|
158
222
|
num_inlets: Optional[int] = None
|
|
159
223
|
num_outlets: Optional[int] = None
|
|
224
|
+
# A carried statement that followed the #X connect lines in the file it was
|
|
225
|
+
# read from, and so must be written back after them.
|
|
226
|
+
after_connections: bool = False
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def occupies_connect_index(self) -> bool:
|
|
230
|
+
"""Whether PureData counts this node when numbering ``#X connect``.
|
|
231
|
+
|
|
232
|
+
Almost everything does. ``#X declare``, ``#A`` array data and ``#X f``
|
|
233
|
+
box widths are statements rather than objects, so they sit in the node
|
|
234
|
+
list without taking an index; a node that returns False here shifts no
|
|
235
|
+
connection.
|
|
236
|
+
"""
|
|
237
|
+
return True
|
|
160
238
|
|
|
161
239
|
class Outlet:
|
|
162
240
|
"""Reference to a specific outlet of a Node, used for creating connections."""
|
|
@@ -399,6 +477,7 @@ class Float(Node):
|
|
|
399
477
|
send: str = "-",
|
|
400
478
|
num_inlets: Optional[int] = 1,
|
|
401
479
|
num_outlets: Optional[int] = 1,
|
|
480
|
+
font_size: Optional[int] = None,
|
|
402
481
|
) -> None:
|
|
403
482
|
self.parameters = {
|
|
404
483
|
"x_pos": x_pos,
|
|
@@ -410,16 +489,18 @@ class Float(Node):
|
|
|
410
489
|
"label": label,
|
|
411
490
|
"receive": receive,
|
|
412
491
|
"send": send,
|
|
492
|
+
"font_size": font_size,
|
|
413
493
|
}
|
|
414
494
|
self.num_inlets = num_inlets
|
|
415
495
|
self.num_outlets = num_outlets
|
|
416
496
|
|
|
417
497
|
def __str__(self) -> str:
|
|
418
498
|
p = self.parameters
|
|
499
|
+
tail = "" if p["font_size"] is None else f" {p['font_size']}"
|
|
419
500
|
return (
|
|
420
501
|
f"#X floatatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
421
|
-
f"{p['lower_limit']} {p['upper_limit']} {p['label_pos']} "
|
|
422
|
-
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
502
|
+
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
503
|
+
f"{p['label']} {p['receive']} {p['send']}{tail};\n"
|
|
423
504
|
)
|
|
424
505
|
|
|
425
506
|
@property
|
|
@@ -469,6 +550,9 @@ class Comment(Node):
|
|
|
469
550
|
|
|
470
551
|
|
|
471
552
|
# Default subpatch canvas dimensions (pixels)
|
|
553
|
+
# Horizontal gap applied when spreading a subpatch's inlet/outlet objects so
|
|
554
|
+
# PureData orders them by creation rather than by an arbitrary tie-break.
|
|
555
|
+
SUBPATCH_IO_SPACING = 70
|
|
472
556
|
SUBPATCH_CANVAS_WIDTH = 300
|
|
473
557
|
SUBPATCH_CANVAS_HEIGHT = 180
|
|
474
558
|
|
|
@@ -543,6 +627,12 @@ class Subpatch(Node):
|
|
|
543
627
|
is_graph: bool = False,
|
|
544
628
|
gop_rect: Tuple[float, float, float, float] = (0, 1, 1, 0),
|
|
545
629
|
gop_margins: Optional[Tuple[int, int]] = (0, 0),
|
|
630
|
+
canvas_x: int = 0,
|
|
631
|
+
canvas_y: int = 0,
|
|
632
|
+
canvas_name: str = "(subpatch)",
|
|
633
|
+
canvas_font_size: int = 10,
|
|
634
|
+
open_on_load: int = 0,
|
|
635
|
+
restore_kind: Optional[str] = None,
|
|
546
636
|
) -> None:
|
|
547
637
|
"""Create a subpatch node.
|
|
548
638
|
|
|
@@ -600,6 +690,12 @@ class Subpatch(Node):
|
|
|
600
690
|
"is_graph": is_graph,
|
|
601
691
|
"gop_rect": gop_rect,
|
|
602
692
|
"gop_margins": gop_margins,
|
|
693
|
+
"canvas_x": canvas_x,
|
|
694
|
+
"canvas_y": canvas_y,
|
|
695
|
+
"canvas_name": canvas_name,
|
|
696
|
+
"canvas_font_size": canvas_font_size,
|
|
697
|
+
"open_on_load": open_on_load,
|
|
698
|
+
"restore_kind": restore_kind,
|
|
603
699
|
}
|
|
604
700
|
self.num_inlets = num_inlets
|
|
605
701
|
self.num_outlets = num_outlets
|
|
@@ -607,23 +703,28 @@ class Subpatch(Node):
|
|
|
607
703
|
def __str__(self) -> str:
|
|
608
704
|
p = self.parameters
|
|
609
705
|
coords_line = ""
|
|
610
|
-
|
|
706
|
+
carries_own_coords = any(isinstance(n, Coords) for n in self.src.nodes)
|
|
707
|
+
if p["graph_on_parent"] and not carries_own_coords:
|
|
611
708
|
# PureData encodes "hide object name and arguments" in the
|
|
612
709
|
# graph-on-parent flag itself: 1 = shown, 2 = hidden. There is no
|
|
613
710
|
# separate field, and the two values after the flag are the
|
|
614
711
|
# viewport margins.
|
|
615
712
|
gop_flag = 2 if p["hide_name"] else 1
|
|
616
|
-
rect = " ".join(
|
|
713
|
+
rect = " ".join(_fmt_num(v) for v in p.get("gop_rect", (0, 1, 1, 0)))
|
|
617
714
|
margins = p.get("gop_margins", (0, 0))
|
|
618
715
|
tail = "" if margins is None else f" {margins[0]} {margins[1]}"
|
|
619
716
|
coords_line = f"#X coords {rect} {p['gop_width']} {p['gop_height']} {gop_flag}{tail};\n"
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
717
|
+
# A parsed subpatch keeps the kind PureData wrote ("page" and others
|
|
718
|
+
# exist); one the builder created derives it from is_graph. A graph
|
|
719
|
+
# canvas closes without a name.
|
|
720
|
+
kind = p.get("restore_kind") or ("graph" if p.get("is_graph") else "pd")
|
|
721
|
+
name = "" if kind == "graph" else p["name"]
|
|
722
|
+
suffix = f" {name}" if name else ""
|
|
723
|
+
restore = f"#X restore {p['x_pos']} {p['y_pos']} {kind}{suffix};\n"
|
|
625
724
|
return (
|
|
626
|
-
f"#N canvas
|
|
725
|
+
f"#N canvas {p['canvas_x']} {p['canvas_y']} "
|
|
726
|
+
f"{self.canvas_width} {self.canvas_height} "
|
|
727
|
+
f"{p['canvas_name']} {p['open_on_load']};\n"
|
|
627
728
|
f"{self.src._subpatch_str()}"
|
|
628
729
|
f"{coords_line}"
|
|
629
730
|
f"{restore}"
|
|
@@ -778,6 +879,89 @@ class Array(Node):
|
|
|
778
879
|
return f"Array({p['name']!r}, {p['length']})"
|
|
779
880
|
|
|
780
881
|
|
|
882
|
+
class Raw(Node):
|
|
883
|
+
"""A statement py2pd has no builder node for, carried verbatim.
|
|
884
|
+
|
|
885
|
+
``#X scalar``, ``#X listbox``, ``#A`` array data and ``#X f`` box widths
|
|
886
|
+
have no builder representation, but a patch read with ``to_builder()`` must
|
|
887
|
+
still write back what it read. The text is stored exactly as parsed.
|
|
888
|
+
|
|
889
|
+
Parameters
|
|
890
|
+
----------
|
|
891
|
+
text : str
|
|
892
|
+
The statement without its trailing semicolon.
|
|
893
|
+
is_object : bool
|
|
894
|
+
Whether PureData counts the statement as an object on the canvas, which
|
|
895
|
+
decides whether it takes a ``#X connect`` index.
|
|
896
|
+
"""
|
|
897
|
+
|
|
898
|
+
def __init__(self, text: str, is_object: bool = False) -> None:
|
|
899
|
+
self.hidden = True
|
|
900
|
+
self.parameters = {"text": text, "is_object": is_object}
|
|
901
|
+
self.num_inlets = None
|
|
902
|
+
self.num_outlets = None
|
|
903
|
+
|
|
904
|
+
@property
|
|
905
|
+
def occupies_connect_index(self) -> bool:
|
|
906
|
+
return bool(self.parameters["is_object"])
|
|
907
|
+
|
|
908
|
+
def __str__(self) -> str:
|
|
909
|
+
return f"{self.parameters['text']};\n"
|
|
910
|
+
|
|
911
|
+
def __repr__(self) -> str:
|
|
912
|
+
return f"Raw({self.parameters['text'][:40]!r})"
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
class Declare(Node):
|
|
916
|
+
"""A ``#X declare`` statement (search paths and libraries).
|
|
917
|
+
|
|
918
|
+
Wraps the AST's ``PdDeclare`` rather than re-modelling it, so the builder
|
|
919
|
+
writes back exactly what the parser read and ``extract_declare_paths()``
|
|
920
|
+
keeps working on a patch that went through the builder.
|
|
921
|
+
"""
|
|
922
|
+
|
|
923
|
+
def __init__(self, declare: "PdDeclare") -> None:
|
|
924
|
+
self.hidden = True
|
|
925
|
+
self.parameters = {"declare": declare}
|
|
926
|
+
self.num_inlets = 0
|
|
927
|
+
self.num_outlets = 0
|
|
928
|
+
|
|
929
|
+
@property
|
|
930
|
+
def occupies_connect_index(self) -> bool:
|
|
931
|
+
return False
|
|
932
|
+
|
|
933
|
+
def __str__(self) -> str:
|
|
934
|
+
return f"{self.parameters['declare']}\n"
|
|
935
|
+
|
|
936
|
+
def __repr__(self) -> str:
|
|
937
|
+
return f"Declare({self.parameters['declare']!r})"
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class Coords(Node):
|
|
941
|
+
"""An ``#X coords`` statement that no subpatch consumed.
|
|
942
|
+
|
|
943
|
+
A subpatch folds its own ``#X coords`` into graph-on-parent parameters. One
|
|
944
|
+
on the top-level canvas, or a second one inside a subpatch, has nowhere to
|
|
945
|
+
go, so it is carried as its parsed ``PdCoords``.
|
|
946
|
+
"""
|
|
947
|
+
|
|
948
|
+
def __init__(self, coords: "PdCoords") -> None:
|
|
949
|
+
self.hidden = True
|
|
950
|
+
self.parameters = {"coords": coords}
|
|
951
|
+
self.num_inlets = 0
|
|
952
|
+
self.num_outlets = 0
|
|
953
|
+
|
|
954
|
+
@property
|
|
955
|
+
def occupies_connect_index(self) -> bool:
|
|
956
|
+
return False
|
|
957
|
+
|
|
958
|
+
def __str__(self) -> str:
|
|
959
|
+
return f"{self.parameters['coords']}\n"
|
|
960
|
+
|
|
961
|
+
def __repr__(self) -> str:
|
|
962
|
+
return f"Coords({self.parameters['coords']!r})"
|
|
963
|
+
|
|
964
|
+
|
|
781
965
|
# An IEM GUI colour is either a legacy packed negative integer (PureData < 0.47)
|
|
782
966
|
# or a hex string such as ``#fcfcfc`` (PureData >= 0.47). Both are accepted and
|
|
783
967
|
# written out unchanged.
|
|
@@ -976,6 +1160,7 @@ class Symbol(Node):
|
|
|
976
1160
|
label: str = "-",
|
|
977
1161
|
receive: str = "-",
|
|
978
1162
|
send: str = "-",
|
|
1163
|
+
font_size: Optional[int] = None,
|
|
979
1164
|
) -> None:
|
|
980
1165
|
self.parameters = {
|
|
981
1166
|
"x_pos": x_pos,
|
|
@@ -987,16 +1172,18 @@ class Symbol(Node):
|
|
|
987
1172
|
"label": label,
|
|
988
1173
|
"receive": receive,
|
|
989
1174
|
"send": send,
|
|
1175
|
+
"font_size": font_size,
|
|
990
1176
|
}
|
|
991
1177
|
self.num_inlets = 1
|
|
992
1178
|
self.num_outlets = 1
|
|
993
1179
|
|
|
994
1180
|
def __str__(self) -> str:
|
|
995
1181
|
p = self.parameters
|
|
1182
|
+
tail = "" if p["font_size"] is None else f" {p['font_size']}"
|
|
996
1183
|
return (
|
|
997
1184
|
f"#X symbolatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
998
|
-
f"{p['lower_limit']} {p['upper_limit']} {p['label_pos']} "
|
|
999
|
-
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
1185
|
+
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
1186
|
+
f"{p['label']} {p['receive']} {p['send']}{tail};\n"
|
|
1000
1187
|
)
|
|
1001
1188
|
|
|
1002
1189
|
@property
|
|
@@ -1073,11 +1260,11 @@ class NumberBox(Node):
|
|
|
1073
1260
|
p = self.parameters
|
|
1074
1261
|
return (
|
|
1075
1262
|
f"#X obj {p['x_pos']} {p['y_pos']} nbx {p['width']} {p['height']} "
|
|
1076
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1263
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1077
1264
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1078
1265
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1079
1266
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1080
|
-
f"{p['init_value']} {p['log_height']};\n"
|
|
1267
|
+
f"{_fmt_num(p['init_value'])} {p['log_height']};\n"
|
|
1081
1268
|
)
|
|
1082
1269
|
|
|
1083
1270
|
@property
|
|
@@ -1151,11 +1338,11 @@ class VSlider(Node):
|
|
|
1151
1338
|
p = self.parameters
|
|
1152
1339
|
return (
|
|
1153
1340
|
f"#X obj {p['x_pos']} {p['y_pos']} vsl {p['width']} {p['height']} "
|
|
1154
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1341
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1155
1342
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1156
1343
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1157
1344
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1158
|
-
f"{p['init_value']} {p['steady']};\n"
|
|
1345
|
+
f"{_fmt_num(p['init_value'])} {p['steady']};\n"
|
|
1159
1346
|
)
|
|
1160
1347
|
|
|
1161
1348
|
@property
|
|
@@ -1226,11 +1413,11 @@ class HSlider(Node):
|
|
|
1226
1413
|
p = self.parameters
|
|
1227
1414
|
return (
|
|
1228
1415
|
f"#X obj {p['x_pos']} {p['y_pos']} hsl {p['width']} {p['height']} "
|
|
1229
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1416
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1230
1417
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1231
1418
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1232
1419
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1233
|
-
f"{p['init_value']} {p['steady']};\n"
|
|
1420
|
+
f"{_fmt_num(p['init_value'])} {p['steady']};\n"
|
|
1234
1421
|
)
|
|
1235
1422
|
|
|
1236
1423
|
@property
|
|
@@ -2213,6 +2400,17 @@ class Patcher:
|
|
|
2213
2400
|
self.canvas_height = canvas_height
|
|
2214
2401
|
self.font_size = font_size
|
|
2215
2402
|
self._node_positions: Dict[int, int] = {}
|
|
2403
|
+
# Statements above the #N canvas line (#N struct and friends), carried
|
|
2404
|
+
# verbatim so a parsed patch writes back what it read.
|
|
2405
|
+
self.preamble: List[str] = []
|
|
2406
|
+
# Set once a node that takes no #X connect index is added. While it is
|
|
2407
|
+
# False the connect index is the list position, which is the common case.
|
|
2408
|
+
self._has_unindexed = False
|
|
2409
|
+
self._connect_prefix: List[int] = []
|
|
2410
|
+
self._connect_prefix_len = -1
|
|
2411
|
+
# Nodes whose x the caller set. Subpatch I/O ordering may reposition
|
|
2412
|
+
# the others; it must never move one the caller placed.
|
|
2413
|
+
self._explicit_x: Set[int] = set()
|
|
2216
2414
|
|
|
2217
2415
|
@property
|
|
2218
2416
|
def row_head(self) -> Optional[Node]:
|
|
@@ -2258,10 +2456,13 @@ class Patcher:
|
|
|
2258
2456
|
) -> Tuple[int, int, Callable[[Node], None]]:
|
|
2259
2457
|
"""Resolve position for a new element."""
|
|
2260
2458
|
was_absolute = x_pos >= 0 and y_pos >= 0
|
|
2459
|
+
x_was_given = x_pos >= 0
|
|
2261
2460
|
computed_x, computed_y = self.layout.compute_position(new_row, new_col, x_pos, y_pos)
|
|
2262
2461
|
|
|
2263
2462
|
def position_update(node: Node) -> None:
|
|
2264
2463
|
self.layout.register_node(node, new_row, new_col, was_absolute)
|
|
2464
|
+
if x_was_given:
|
|
2465
|
+
self._explicit_x.add(id(node))
|
|
2265
2466
|
|
|
2266
2467
|
return (computed_x, computed_y, position_update)
|
|
2267
2468
|
|
|
@@ -2492,6 +2693,12 @@ class Patcher:
|
|
|
2492
2693
|
is_graph: bool = False,
|
|
2493
2694
|
gop_rect: Tuple[float, float, float, float] = (0, 1, 1, 0),
|
|
2494
2695
|
gop_margins: Optional[Tuple[int, int]] = (0, 0),
|
|
2696
|
+
canvas_x: int = 0,
|
|
2697
|
+
canvas_y: int = 0,
|
|
2698
|
+
canvas_name: str = "(subpatch)",
|
|
2699
|
+
canvas_font_size: int = 10,
|
|
2700
|
+
open_on_load: int = 0,
|
|
2701
|
+
restore_kind: Optional[str] = None,
|
|
2495
2702
|
) -> Subpatch:
|
|
2496
2703
|
"""Add a subpatch to the patch.
|
|
2497
2704
|
|
|
@@ -2576,6 +2783,9 @@ class Patcher:
|
|
|
2576
2783
|
and n.parameters["text"].split()[:1] in (["outlet"], ["outlet~"])
|
|
2577
2784
|
)
|
|
2578
2785
|
|
|
2786
|
+
_order_subpatch_io(src)
|
|
2787
|
+
_warn_on_ambiguous_subpatch_io(src, name)
|
|
2788
|
+
|
|
2579
2789
|
x_pos, y_pos, pos_update = self._resolve_position(x_pos, y_pos, new_row, new_col)
|
|
2580
2790
|
node = Subpatch(
|
|
2581
2791
|
x_pos,
|
|
@@ -2593,6 +2803,12 @@ class Patcher:
|
|
|
2593
2803
|
is_graph=is_graph,
|
|
2594
2804
|
gop_rect=gop_rect,
|
|
2595
2805
|
gop_margins=gop_margins,
|
|
2806
|
+
canvas_x=canvas_x,
|
|
2807
|
+
canvas_y=canvas_y,
|
|
2808
|
+
canvas_name=canvas_name,
|
|
2809
|
+
canvas_font_size=canvas_font_size,
|
|
2810
|
+
open_on_load=open_on_load,
|
|
2811
|
+
restore_kind=restore_kind,
|
|
2596
2812
|
)
|
|
2597
2813
|
self._register(node, pos_update)
|
|
2598
2814
|
return node
|
|
@@ -2654,7 +2870,9 @@ class Patcher:
|
|
|
2654
2870
|
self._register(node, pos_update)
|
|
2655
2871
|
return node
|
|
2656
2872
|
|
|
2657
|
-
def add_array(
|
|
2873
|
+
def add_array(
|
|
2874
|
+
self, name: str, length: int, element_type: str = "float", save_flag: int = 0
|
|
2875
|
+
) -> Array:
|
|
2658
2876
|
"""Declare an array in the subpatch.
|
|
2659
2877
|
|
|
2660
2878
|
Parameters
|
|
@@ -2665,6 +2883,12 @@ class Patcher:
|
|
|
2665
2883
|
length : int
|
|
2666
2884
|
the array length
|
|
2667
2885
|
|
|
2886
|
+
element_type : str
|
|
2887
|
+
the array's data type (default ``'float'``)
|
|
2888
|
+
|
|
2889
|
+
save_flag : int
|
|
2890
|
+
whether PureData saves the contents with the patch (default 0)
|
|
2891
|
+
|
|
2668
2892
|
Returns
|
|
2669
2893
|
-------
|
|
2670
2894
|
node : Array
|
|
@@ -2672,9 +2896,9 @@ class Patcher:
|
|
|
2672
2896
|
|
|
2673
2897
|
Notes
|
|
2674
2898
|
-----
|
|
2675
|
-
The array will not have a graph.
|
|
2899
|
+
The array will not have a graph.
|
|
2676
2900
|
"""
|
|
2677
|
-
node = Array(name, length)
|
|
2901
|
+
node = Array(name, length, element_type, save_flag)
|
|
2678
2902
|
self._register(node)
|
|
2679
2903
|
return node
|
|
2680
2904
|
|
|
@@ -3359,8 +3583,8 @@ class Patcher:
|
|
|
3359
3583
|
outlet = source.index
|
|
3360
3584
|
source = source.owner
|
|
3361
3585
|
|
|
3362
|
-
source_index = self.
|
|
3363
|
-
sink_index = self.
|
|
3586
|
+
source_index = self._connect_index_of(source, "Source")
|
|
3587
|
+
sink_index = self._connect_index_of(sink, "Sink")
|
|
3364
3588
|
|
|
3365
3589
|
if outlet < 0:
|
|
3366
3590
|
raise PdConnectionError(f"Outlet index must be non-negative, got {outlet}")
|
|
@@ -3380,6 +3604,62 @@ class Patcher:
|
|
|
3380
3604
|
|
|
3381
3605
|
self.connections.append(Connection(source_index, outlet, sink_index, inlet))
|
|
3382
3606
|
|
|
3607
|
+
def _connect_index_of(self, node: Node, role: str) -> int:
|
|
3608
|
+
"""Return *node*'s ``#X connect`` index.
|
|
3609
|
+
|
|
3610
|
+
Equal to the list position until a node that takes no index is added,
|
|
3611
|
+
after which it is the count of index-taking nodes before it. The prefix
|
|
3612
|
+
sums are cached against the node count, so the link pass over a parsed
|
|
3613
|
+
patch rebuilds them once rather than per connection.
|
|
3614
|
+
"""
|
|
3615
|
+
position = self._index_of(node, role)
|
|
3616
|
+
if not self._has_unindexed:
|
|
3617
|
+
return position
|
|
3618
|
+
if self._connect_prefix_len != len(self.nodes):
|
|
3619
|
+
prefix: List[int] = []
|
|
3620
|
+
count = 0
|
|
3621
|
+
for existing in self.nodes:
|
|
3622
|
+
prefix.append(count)
|
|
3623
|
+
if existing.occupies_connect_index:
|
|
3624
|
+
count += 1
|
|
3625
|
+
self._connect_prefix = prefix
|
|
3626
|
+
self._connect_prefix_len = len(self.nodes)
|
|
3627
|
+
return self._connect_prefix[position]
|
|
3628
|
+
|
|
3629
|
+
def add_raw(self, text: str, is_object: bool = False, after_connections: bool = False) -> Raw:
|
|
3630
|
+
"""Append a statement the builder does not model, written back verbatim.
|
|
3631
|
+
|
|
3632
|
+
Parameters
|
|
3633
|
+
----------
|
|
3634
|
+
text : str
|
|
3635
|
+
The statement without its trailing semicolon.
|
|
3636
|
+
is_object : bool
|
|
3637
|
+
Whether PureData counts it as an object, and so whether it takes a
|
|
3638
|
+
``#X connect`` index.
|
|
3639
|
+
"""
|
|
3640
|
+
node = Raw(text, is_object)
|
|
3641
|
+
node.after_connections = after_connections
|
|
3642
|
+
if not node.occupies_connect_index:
|
|
3643
|
+
self._has_unindexed = True
|
|
3644
|
+
self._register(node)
|
|
3645
|
+
return node
|
|
3646
|
+
|
|
3647
|
+
def add_declare(self, declare: PdDeclare, after_connections: bool = False) -> Declare:
|
|
3648
|
+
"""Append a ``#X declare`` statement, written back verbatim."""
|
|
3649
|
+
node = Declare(declare)
|
|
3650
|
+
node.after_connections = after_connections
|
|
3651
|
+
self._has_unindexed = True
|
|
3652
|
+
self._register(node)
|
|
3653
|
+
return node
|
|
3654
|
+
|
|
3655
|
+
def add_coords(self, coords: PdCoords, after_connections: bool = False) -> Coords:
|
|
3656
|
+
"""Append an ``#X coords`` statement no subpatch consumed."""
|
|
3657
|
+
node = Coords(coords)
|
|
3658
|
+
node.after_connections = after_connections
|
|
3659
|
+
self._has_unindexed = True
|
|
3660
|
+
self._register(node)
|
|
3661
|
+
return node
|
|
3662
|
+
|
|
3383
3663
|
def _index_of(self, node: Node, role: str) -> int:
|
|
3384
3664
|
"""Return *node*'s position in ``self.nodes``.
|
|
3385
3665
|
|
|
@@ -3415,20 +3695,27 @@ class Patcher:
|
|
|
3415
3695
|
add_link = link
|
|
3416
3696
|
|
|
3417
3697
|
def __str__(self) -> str:
|
|
3698
|
+
preamble = "".join(f"{line};\n" for line in self.preamble)
|
|
3418
3699
|
canvas = (
|
|
3419
3700
|
f"#N canvas {self.canvas_x} {self.canvas_y} "
|
|
3420
3701
|
f"{self.canvas_width} {self.canvas_height} {self.font_size};\n"
|
|
3421
3702
|
)
|
|
3422
|
-
return f"{canvas}{self._subpatch_str().rstrip()}"
|
|
3703
|
+
return f"{preamble}{canvas}{self._subpatch_str().rstrip()}"
|
|
3423
3704
|
|
|
3424
3705
|
def __repr__(self) -> str:
|
|
3425
3706
|
return f"Patcher(nodes={len(self.nodes)}, connections={len(self.connections)})"
|
|
3426
3707
|
|
|
3427
3708
|
def _subpatch_str(self) -> str:
|
|
3428
|
-
"""Internal: generate string for patch contents.
|
|
3429
|
-
|
|
3709
|
+
"""Internal: generate string for patch contents.
|
|
3710
|
+
|
|
3711
|
+
A statement read from after the ``#X connect`` block goes back there.
|
|
3712
|
+
PureData writes canvas properties such as ``#X coords`` below the
|
|
3713
|
+
connections, and moving one above them changes the file it wrote.
|
|
3714
|
+
"""
|
|
3715
|
+
nodes_str = "".join(str(n) for n in self.nodes if not n.after_connections)
|
|
3430
3716
|
connections_str = "".join(str(c) for c in self.connections)
|
|
3431
|
-
|
|
3717
|
+
trailing_str = "".join(str(n) for n in self.nodes if n.after_connections)
|
|
3718
|
+
return f"{nodes_str}{connections_str}{trailing_str}"
|
|
3432
3719
|
|
|
3433
3720
|
def save(
|
|
3434
3721
|
self,
|
|
@@ -782,10 +782,10 @@ class ParseError(Exception):
|
|
|
782
782
|
class UnsupportedElementWarning(UserWarning):
|
|
783
783
|
"""Warning issued when converting to the builder API would lose an element.
|
|
784
784
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
785
|
+
Statements the builder does not model -- data structure templates, scalars,
|
|
786
|
+
array data, box widths -- are carried verbatim by ``Raw``, ``Declare`` and
|
|
787
|
+
``Coords`` nodes, so they no longer raise this. It is left for a connection
|
|
788
|
+
whose endpoint has no builder node at all.
|
|
789
789
|
"""
|
|
790
790
|
|
|
791
791
|
pass
|
|
@@ -1512,13 +1512,16 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1512
1512
|
patch.font_size,
|
|
1513
1513
|
)
|
|
1514
1514
|
elements: List[PdElement] = []
|
|
1515
|
+
# Carried statements PureData wrote below the #X connect block.
|
|
1516
|
+
trailing: List[PdElement] = []
|
|
1515
1517
|
|
|
1516
1518
|
for node in patch.nodes:
|
|
1517
1519
|
if isinstance(node, api.Obj):
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1520
|
+
# The parser's tokenizer, not str.split(): a plain whitespace split
|
|
1521
|
+
# eats an escaped trailing space, rewriting the object's last atom.
|
|
1522
|
+
tokens = _tokenize(node.parameters["text"])
|
|
1523
|
+
class_name = tokens[0] if tokens else ""
|
|
1524
|
+
args = tuple(tokens[1:])
|
|
1522
1525
|
pos = Position(node.parameters["x_pos"], node.parameters["y_pos"])
|
|
1523
1526
|
elements.append(PdObj(pos, class_name, args))
|
|
1524
1527
|
|
|
@@ -1539,13 +1542,23 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1539
1542
|
p["width"],
|
|
1540
1543
|
p["lower_limit"],
|
|
1541
1544
|
p["upper_limit"],
|
|
1542
|
-
|
|
1545
|
+
p["label_pos"],
|
|
1543
1546
|
p["label"],
|
|
1544
1547
|
p["receive"],
|
|
1545
1548
|
p["send"],
|
|
1549
|
+
p["font_size"],
|
|
1546
1550
|
)
|
|
1547
1551
|
)
|
|
1548
1552
|
|
|
1553
|
+
elif isinstance(node, (api.Raw, api.Declare, api.Coords)):
|
|
1554
|
+
if isinstance(node, api.Raw):
|
|
1555
|
+
carried: PdElement = PdRaw(node.parameters["text"], node.parameters["is_object"])
|
|
1556
|
+
elif isinstance(node, api.Declare):
|
|
1557
|
+
carried = node.parameters["declare"]
|
|
1558
|
+
else:
|
|
1559
|
+
carried = node.parameters["coords"]
|
|
1560
|
+
(trailing if node.after_connections else elements).append(carried)
|
|
1561
|
+
|
|
1549
1562
|
elif isinstance(node, api.Array):
|
|
1550
1563
|
p = node.parameters
|
|
1551
1564
|
elements.append(PdArray(p["name"], p["length"], p["element_type"], p["save_flag"]))
|
|
@@ -1556,15 +1569,19 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1556
1569
|
p = node.parameters
|
|
1557
1570
|
pos = Position(p["x_pos"], p["y_pos"])
|
|
1558
1571
|
subpatch_canvas = CanvasProperties(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1572
|
+
p["canvas_x"],
|
|
1573
|
+
p["canvas_y"],
|
|
1574
|
+
node.canvas_width,
|
|
1575
|
+
node.canvas_height,
|
|
1576
|
+
p["canvas_font_size"],
|
|
1577
|
+
p["canvas_name"],
|
|
1578
|
+
p["open_on_load"],
|
|
1565
1579
|
)
|
|
1580
|
+
kind = p.get("restore_kind") or ("graph" if p.get("is_graph") else "pd")
|
|
1581
|
+
restore = PdRestore(pos, "" if kind == "graph" else p["name"], kind)
|
|
1566
1582
|
inner_elements = list(inner_ast.elements)
|
|
1567
|
-
|
|
1583
|
+
carries_own_coords = any(isinstance(e, PdCoords) for e in inner_elements)
|
|
1584
|
+
if p["graph_on_parent"] and not carries_own_coords:
|
|
1568
1585
|
rect = p.get("gop_rect", (0, 1, 1, 0))
|
|
1569
1586
|
margins = p.get("gop_margins", (0, 0))
|
|
1570
1587
|
inner_elements.append(
|
|
@@ -1641,6 +1658,7 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1641
1658
|
p["label"],
|
|
1642
1659
|
p["receive"],
|
|
1643
1660
|
p["send"],
|
|
1661
|
+
p["font_size"],
|
|
1644
1662
|
)
|
|
1645
1663
|
)
|
|
1646
1664
|
|
|
@@ -1817,8 +1835,9 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1817
1835
|
# Add connections
|
|
1818
1836
|
for conn in patch.connections:
|
|
1819
1837
|
elements.append(PdConnect(conn.source, conn.outlet_index, conn.sink, conn.inlet_index))
|
|
1838
|
+
elements.extend(trailing)
|
|
1820
1839
|
|
|
1821
|
-
return PdPatch(canvas, elements)
|
|
1840
|
+
return PdPatch(canvas, elements, [PdRaw(line) for line in patch.preamble])
|
|
1822
1841
|
|
|
1823
1842
|
|
|
1824
1843
|
def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
@@ -1853,9 +1872,14 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1853
1872
|
font_size=ast.canvas.font_size,
|
|
1854
1873
|
)
|
|
1855
1874
|
|
|
1875
|
+
patch.preamble = [raw.text for raw in ast.preamble]
|
|
1876
|
+
|
|
1856
1877
|
# First pass: create all nodes (non-connections)
|
|
1857
1878
|
node_map: List[Optional[api.Node]] = [] # Track nodes for linking
|
|
1858
1879
|
node: api.Node
|
|
1880
|
+
# PureData writes canvas properties below the #X connect block. A carried
|
|
1881
|
+
# statement remembers which side of it that block it came from.
|
|
1882
|
+
seen_connect = False
|
|
1859
1883
|
for elem in ast.elements:
|
|
1860
1884
|
if isinstance(elem, PdObj):
|
|
1861
1885
|
# elem.text is already in PureData's escaped form; escaping it a
|
|
@@ -1881,6 +1905,7 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1881
1905
|
label=elem.label,
|
|
1882
1906
|
receive=elem.receive,
|
|
1883
1907
|
send=elem.send,
|
|
1908
|
+
font_size=elem.font_size,
|
|
1884
1909
|
)
|
|
1885
1910
|
patch.nodes.append(node)
|
|
1886
1911
|
node_map.append(node)
|
|
@@ -1896,6 +1921,7 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1896
1921
|
label=elem.label,
|
|
1897
1922
|
receive=elem.receive,
|
|
1898
1923
|
send=elem.send,
|
|
1924
|
+
font_size=elem.font_size,
|
|
1899
1925
|
)
|
|
1900
1926
|
patch.nodes.append(node)
|
|
1901
1927
|
node_map.append(node)
|
|
@@ -1906,12 +1932,10 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1906
1932
|
node_map.append(node)
|
|
1907
1933
|
|
|
1908
1934
|
elif isinstance(elem, PdArray):
|
|
1909
|
-
node = patch.add_array(elem.name, elem.size)
|
|
1935
|
+
node = patch.add_array(elem.name, elem.size, elem.dtype, elem.save_flag)
|
|
1910
1936
|
node_map.append(node)
|
|
1911
1937
|
|
|
1912
1938
|
elif isinstance(elem, PdSubpatch):
|
|
1913
|
-
# Recursively convert subpatch
|
|
1914
|
-
inner_patch = to_builder(PdPatch(elem.canvas, elem.elements))
|
|
1915
1939
|
name = elem.restore.name if elem.restore else "subpatch"
|
|
1916
1940
|
pos = elem.restore.position if elem.restore else Position(0, 0)
|
|
1917
1941
|
# Extract GOP settings from PdCoords if present
|
|
@@ -1934,6 +1958,10 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1934
1958
|
else (sub_elem.x_margin or 0, sub_elem.y_margin or 0)
|
|
1935
1959
|
)
|
|
1936
1960
|
break
|
|
1961
|
+
# The folded coords stays in the recursion as a carried node: the
|
|
1962
|
+
# writers below regenerate one only when the inner patch has none,
|
|
1963
|
+
# which keeps a parsed coords at the line PureData wrote it on.
|
|
1964
|
+
inner_patch = to_builder(PdPatch(elem.canvas, elem.elements))
|
|
1937
1965
|
if elem.restore is not None and elem.restore.is_graph:
|
|
1938
1966
|
gop_kwargs["is_graph"] = True
|
|
1939
1967
|
node = patch.add_subpatch(
|
|
@@ -1943,6 +1971,12 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1943
1971
|
y_pos=pos.y,
|
|
1944
1972
|
canvas_width=elem.canvas.width,
|
|
1945
1973
|
canvas_height=elem.canvas.height,
|
|
1974
|
+
canvas_x=elem.canvas.x,
|
|
1975
|
+
canvas_y=elem.canvas.y,
|
|
1976
|
+
canvas_name=elem.canvas.name if elem.canvas.name is not None else "(subpatch)",
|
|
1977
|
+
canvas_font_size=elem.canvas.font_size,
|
|
1978
|
+
open_on_load=elem.canvas.open_on_load,
|
|
1979
|
+
restore_kind=elem.restore.kind if elem.restore is not None else None,
|
|
1946
1980
|
**gop_kwargs,
|
|
1947
1981
|
)
|
|
1948
1982
|
node_map.append(node)
|
|
@@ -2155,26 +2189,26 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
2155
2189
|
node_map.append(node)
|
|
2156
2190
|
|
|
2157
2191
|
elif isinstance(elem, PdRaw):
|
|
2158
|
-
# The builder
|
|
2159
|
-
#
|
|
2160
|
-
|
|
2161
|
-
kind = " ".join(elem.text.split()[:2]) or "?"
|
|
2162
|
-
warnings.warn(
|
|
2163
|
-
f"to_builder() cannot represent {kind!r} statements; "
|
|
2164
|
-
f"dropping: {elem.text[:60]!r}. Use the AST API to preserve them.",
|
|
2165
|
-
UnsupportedElementWarning,
|
|
2166
|
-
stacklevel=2,
|
|
2167
|
-
)
|
|
2192
|
+
# The builder models no such statement, but it must still write back
|
|
2193
|
+
# what it read, so the text is carried verbatim.
|
|
2194
|
+
node = patch.add_raw(elem.text, elem.is_object, after_connections=seen_connect)
|
|
2168
2195
|
# Only occupy a connect index if Pd counts the statement as an
|
|
2169
2196
|
# object, otherwise every following index shifts.
|
|
2170
2197
|
if elem.is_object:
|
|
2171
|
-
node_map.append(
|
|
2198
|
+
node_map.append(node)
|
|
2199
|
+
|
|
2200
|
+
elif isinstance(elem, PdDeclare):
|
|
2201
|
+
# Not an object, so it consumes no connect index.
|
|
2202
|
+
patch.add_declare(elem, after_connections=seen_connect)
|
|
2203
|
+
|
|
2204
|
+
elif isinstance(elem, PdCoords):
|
|
2205
|
+
# A subpatch strips the coords it folds into its own parameters
|
|
2206
|
+
# before recursing, so anything reaching here belongs to this canvas.
|
|
2207
|
+
patch.add_coords(elem, after_connections=seen_connect)
|
|
2172
2208
|
|
|
2173
|
-
elif isinstance(elem,
|
|
2174
|
-
#
|
|
2175
|
-
|
|
2176
|
-
# the enclosing subpatch; declare has no builder equivalent.
|
|
2177
|
-
pass
|
|
2209
|
+
elif isinstance(elem, PdConnect):
|
|
2210
|
+
# Not an object, and handled in the second pass.
|
|
2211
|
+
seen_connect = True
|
|
2178
2212
|
|
|
2179
2213
|
else:
|
|
2180
2214
|
node_map.append(None) # Placeholder for unknown elements
|
|
@@ -2231,7 +2265,7 @@ def transform(patch: PdPatch, transformer: ElementTransformer) -> PdPatch:
|
|
|
2231
2265
|
if transformed is not None:
|
|
2232
2266
|
new_elements.append(transformed)
|
|
2233
2267
|
|
|
2234
|
-
return PdPatch(patch.canvas, new_elements)
|
|
2268
|
+
return PdPatch(patch.canvas, new_elements, patch.preamble)
|
|
2235
2269
|
|
|
2236
2270
|
|
|
2237
2271
|
def find_objects(patch: PdPatch, predicate: ElementPredicate) -> List[PdElement]:
|
|
@@ -12,7 +12,7 @@ import sys
|
|
|
12
12
|
from typing import Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from .api import _infer_abstraction_io
|
|
15
|
-
from .ast import PdDeclare, PdElement, PdPatch, PdSubpatch
|
|
15
|
+
from .ast import ParseError, PdDeclare, PdElement, PdPatch, PdSubpatch
|
|
16
16
|
|
|
17
17
|
# Maps sys.platform prefix to recognized binary extensions for externals
|
|
18
18
|
_EXTERNAL_EXTENSIONS = {
|
|
@@ -98,6 +98,7 @@ def discover_externals(
|
|
|
98
98
|
Mapping of external name to (num_inlets, num_outlets).
|
|
99
99
|
Binary externals have (None, None) since I/O cannot be inferred.
|
|
100
100
|
First-found wins when the same name appears in multiple paths.
|
|
101
|
+
A ``.pd`` file that cannot be read or parsed is skipped.
|
|
101
102
|
"""
|
|
102
103
|
paths: List[str] = []
|
|
103
104
|
if search_paths:
|
|
@@ -130,7 +131,9 @@ def discover_externals(
|
|
|
130
131
|
try:
|
|
131
132
|
inlets, outlets = _infer_abstraction_io(full_path)
|
|
132
133
|
registry[name] = (inlets, outlets)
|
|
133
|
-
except OSError:
|
|
134
|
+
except (OSError, ParseError):
|
|
135
|
+
# An unreadable or unparseable candidate is an unusable
|
|
136
|
+
# candidate, not a reason to abandon the whole scan.
|
|
134
137
|
continue
|
|
135
138
|
continue
|
|
136
139
|
|
|
@@ -220,13 +220,13 @@ def validate_patch(
|
|
|
220
220
|
TypeError
|
|
221
221
|
If *patch* is not a ``Patcher`` or ``PdPatch``.
|
|
222
222
|
"""
|
|
223
|
-
_ensure_libpd()
|
|
224
|
-
|
|
225
|
-
import cypd
|
|
226
|
-
|
|
227
223
|
content = _serialize_input(patch)
|
|
228
224
|
|
|
229
225
|
with _libpd_lock:
|
|
226
|
+
_ensure_libpd()
|
|
227
|
+
|
|
228
|
+
import cypd
|
|
229
|
+
|
|
230
230
|
return _validate_locked(
|
|
231
231
|
cypd,
|
|
232
232
|
content,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|