#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# **************************************************************************** #
# This file is part of the pdpy project: https://github.com/pdpy-org
# Copyright (C) 2021-22 Fede Camara Halac
# **************************************************************************** #
"""
PdPy
====
"""
from . import canvas, dependencies
from ..connections.edge import Edge
from ..core.base import Base
from ..core.canvasbase import CanvasBase
from ..primitives.point import Point
from ..primitives.size import Size
from ..primitives.coords import Coords
from ..iemgui.cnv import Cnv
from ..iemgui.toggle import Toggle
from ..iemgui.slider import Slider
from ..iemgui.radio import Radio
from ..iemgui.nbx import Nbx
from ..iemgui.bng import Bng
from ..iemgui.vu import Vu
from ..memory.struct import Struct
from ..memory.scalar import Scalar
from ..memory.graph import Graph
from ..memory.goparray import GOPArray
from ..memory.data import Data
from ..memory.array import Array
from ..objects.obj import Obj
from ..objects.msg import Msg
from ..objects.gui import Gui
from ..objects.comment import Comment
from ..utilities.utils import log
from ..utilities.default import *
__all__ = [ 'PdPy' ]
[docs]class PdPy(CanvasBase, Base):
def __init__(self,
name=None,
encoding='utf-8',
root=False,
pd_lines=None,
json=None,
xml=None,
pdpath=None):
""" Initialize a PdPy object """
self.patchname = Base.__sane_name__(self, name)
self.encoding = encoding
self.__pdpy__ = self.__class__.__name__
self.__canvas_idx__ = []
self.__depth__ = 0
self.__max_w__ = 0
self.__max_h__ = 0
self.arrangement(5) # set the arrangement function
# The following are used in the arrange function:
# the horizontal step size for increments
self.__hstep__ = 1.25
# the vertical step size for increments
self.__vstep__ = 1
CanvasBase.__init__(self, obj_idx=0)
Base.__init__(self, json=json, xml=xml)
if json is None and xml is None and pd_lines is not None:
# parse the pd lines and populate the pdpy instance
# account for pure data line endings and split into a list
self.parse(pd_lines)
if root:
self.root = canvas.Canvas()
self.root.isroot = True
self.root.patchname = self.patchname
if hasattr(self, 'root'):
# update the parent of all childs
self.__jsontree__()
self.__set_pd_path__(pdpath)
def __jsontree__(self):
""" Spawn a json tree structure adding parents to every child """
# log(0, f"{self.__class__.__name__}.__jsontree__()")
setattr(self.root, '__p__', self)
for x in getattr(self, 'structs', []):
setattr(x, '__p__', self)
self.__addparents__(self.root)
[docs] def getTemplate(self, template_name):
""" Get the template related to this object's Struct """
for idx, s in enumerate(getattr(self, 'structs')):
if template_name == getattr(s, 'name'):
return idx, s
[docs] def addStruct(self, pd_lines=None, json=None, xml=None):
""" Add a Struct object from pure data syntax tokens """
if not hasattr(self, 'structs'):
self.structs = []
struct = Struct(pd_lines=pd_lines, json=json, xml=xml)
struct.__parent__(self)
self.structs.append(struct)
[docs] def addRoot(self, pd_lines=None, json=None, name=None):
""" Add a root canvas object from pure data, json, or an empty canvas
Called by the :func:`parse` method as well as the :class:`pdpy_lib.parse.pdpyparser.PdPyParser` class
Return
------
The root canvas object
"""
if pd_lines is not None:
self.root = canvas.Canvas()
for x in [ ('screen', Point(x=pd_lines[0], y=pd_lines[1])),
('dimension', Size(w=pd_lines[2], h=pd_lines[3])),
('font', int(pd_lines[4])) ]:
setattr(self.root, x[0], x[1])
elif json is not None:
self.root = canvas.Canvas(json=json)
else:
self.root = canvas.Canvas()
# Force these things to happen
for x in [ ('__p__', self),
('id', None),
('isroot', True),
('vis', 1),
('name', name or self.patchname) ]:
setattr(self.root, x[0], x[1])
return self.root
[docs] def addDependencies(self, **kwargs):
""" Handle dependencies called with ``declare`` """
if not hasattr(self,"dependencies"):
self.dependencies = dependencies.Dependencies(**kwargs)
else:
self.dependencies.update(dependencies.Dependencies(**kwargs))
def __last_canvas__(self):
""" Returns the most recent canvas (from the nodes list)
1. Start at the root canvas
2. Do `__depth__` amount of iterations (0 depth will return `self.root`)
3. Get the `canvas_node_index` by indexing `__canvas_idx__` with `__depth__`
4. Return the canvas located at that `canvas_node_index`
"""
__canvas__ = self.root
for idx in range(self.__depth__):
canvas_node_index = self.__canvas_idx__[idx]
__canvas__ = __canvas__.nodes[canvas_node_index]
return __canvas__
def __get_canvas__(self):
""" Return the last canvas taking depth and incrementing object index count """
# __canvas__ = self.root if self.__depth__ == 0 else self.__last_canvas__()
__canvas__ = self.__last_canvas__()
self.__obj_idx__ = __canvas__.grow()
self.__depth__ += 1
return __canvas__
[docs] def addCanvas(self, pd_lines=None, json=None):
""" Add a canvas.Canvas object from pure data syntax tokens """
from .canvas import Canvas
__canvas__ = self.__get_canvas__()
if pd_lines is not None:
canvas = Canvas(json={
'name' : pd_lines[4],
'vis' : self.__num__(pd_lines[5]),
'id' : self.__obj_idx__,
'screen' : Point(x=pd_lines[0], y=pd_lines[1]),
'dimension' : Size(w=pd_lines[2], h=pd_lines[3]),
})
elif json is not None:
canvas = canvas.Canvas(json=json)
else:
canvas = canvas.Canvas()
if not hasattr(canvas, 'id'):
setattr(canvas, 'id', self.__obj_idx__)
self.__canvas_idx__.append(__canvas__.add(canvas))
return canvas
[docs] def addObj(self, argv):
""" Add a Pd object from pure data syntax tokens """
# log(1,"addOBJ", argv)
self.__obj_idx__ = self.__last_canvas__().grow()
if 2 == len(argv):
# an empty object
obj = Obj(pd_lines = [self.__obj_idx__] + argv)
self.__last_canvas__().add(obj)
else:
# protect against argv not being a list
if not isinstance(argv, list):
argv = [argv]
# text-group object
if "text" == argv[2]:
obj = Array(pd_lines = [self.__obj_idx__] + argv)
self.__last_canvas__().add(obj)
# array-group object
elif "array" == argv[2]:
obj = Array(pd_lines = [self.__obj_idx__] + argv)
self.__last_canvas__().add(obj)
# scalar-define object
elif "scalar" == argv[2]:
obj = Array(pd_lines = [self.__obj_idx__] + argv)
self.__last_canvas__().add(obj)
# file-define object
elif "file" == argv[2]:
obj = Array(pd_lines = [self.__obj_idx__] + argv)
self.__last_canvas__().add(obj)
# IEMGUI-group object
elif argv[2] in IEMGuiNames:
# log(1, "NODES:", argv)
if "vu" in argv[2]:
obj = Vu(pd_lines = [self.__obj_idx__] + argv)
elif "tgl" in argv[2]:
obj = Toggle(pd_lines = [self.__obj_idx__] + argv)
elif "cnv" in argv[2] or "my_canvas" in argv[2]:
obj = Cnv(pd_lines = [self.__obj_idx__] + argv)
elif "radio" in argv[2] or "rdb" in argv[2]:
obj = Radio(pd_lines = [self.__obj_idx__] + argv)
elif "bng" in argv[2]:
obj = Bng(pd_lines = [self.__obj_idx__] + argv)
elif "nbx" in argv[2]:
obj = Nbx(pd_lines = [self.__obj_idx__] + argv)
elif "sl" in argv[2]:
obj = Slider(pd_lines = [self.__obj_idx__] + argv)
else:
raise ValueError("Unknown class name: {}".format(self.className))
self.__last_canvas__().add(obj)
# TODO: make special cases for data structures
# - drawing instructions
else:
obj = Obj(pd_lines = [self.__obj_idx__] + argv)
self.__last_canvas__().add(obj)
return obj
[docs] def addMsg(self, argv):
""" Add a Message object from pure data syntax tokens """
# log(1,"msg", nodes)
self.__obj_idx__ = self.__last_canvas__().grow()
msg = Msg(pd_lines=[self.__obj_idx__]+argv)
self.__last_canvas__().add(msg)
return msg
[docs] def addNativeGui(self, className, argv):
""" Add a Gui object from pure data syntax tokens """
self.__obj_idx__ = self.__last_canvas__().grow()
obj = Gui(pd_lines=[ className, self.__obj_idx__ ] + argv)
self.__last_canvas__().add(obj)
return obj
[docs] def addGraph(self, argv):
""" Add the ye-olde array ancestor from pure data syntax tokens """
self.__obj_idx__ = self.__last_canvas__().grow()
graph = Graph(pd_lines=[self.__obj_idx__, argv[0]] + argv[5:9] + argv[1:5])
self.__last_canvas__().add(graph)
return graph
[docs] def addGOPArray(self, argv):
""" Add a Graph-on-Parent Array object from pure data syntax tokens """
# log(1,"addGOPArray", argv)
arr = GOPArray(json={
'name' : argv[0],
'length' : argv[1],
'type' : argv[2],
'flag' : argv[3],
'className' : "goparray"
}, cls='array')
self.__last_canvas__().add(arr)
return arr
[docs] def addScalar(self, pd_lines):
""" Add a Scalar object from pure data syntax tokens """
scalar = Scalar(struct=self.structs, pd_lines=pd_lines)
self.__last_canvas__().add(scalar)
return scalar
[docs] def addEdge(self, pd_lines):
""" Add a connection (Edge) object from pure data syntax tokens """
self.__last_canvas__().edge(Edge(pd_lines=pd_lines))
[docs] def addCoords(self, coords):
""" Adds the Coords class to the canvas """
setattr(self.__last_canvas__(), "coords", Coords(coords=coords))
[docs] def restore(self, argv=None):
""" Restore constructor called from :func:`parse` """
# restored canvases also have borders
# log(0,"RESTORE",argv)
last = self.__last_canvas__()
if argv is not None:
setattr(last, 'position', Point(x=argv[0], y=argv[1]))
setattr(last, 'title', ' '.join(argv[2:]))
self.__depth__ -= 1
if len(self.__canvas_idx__):
self.__canvas_idx__.pop()
return last
[docs] def write(self, filename=None):
""" Write out the pd file to disk """
self.__arrange__(self)
if filename is None:
filename = self.patchname + '.pd'
if '.pd' in filename:
try:
binbuf = self.__pd__()
except Exception as e:
log(3, e, "ERROR WITH BINBUF", self.patchname)
raise Exception(e)
with open(filename, 'w') as patchfile:
patchfile.write(binbuf)
elif '.json' in filename:
with open(filename, 'w') as patchfile:
patchfile.write(self.__json__())
[docs] def parse(self, argvecs):
""" Parse a list of Pd argument vectors (1) into this instance's scope
This method populates the current class with appropriate calls to
individual classes refered to by parsing the argument vector `argv`.
(1) A pure data argument vector is a list containing a tokenized version
of the pure data file line (binbuf), starting with '#' and ending with ';'.
The tokens are split by spaces, ignoring escaped chars. The special char
',' is handled before calling this method.
"""
# log(1,f'Parsing {len(argvecs)} pd_lines')
# print(type(argvecs))
store_graph = False
last = None
for argv in argvecs:
# log(1, "argv:", argv)
head = argv[:2]
body = argv[2:]
# log(1, "head:", head, "body:", body)
if "#N" == head[0]: #N -> either structs or canvases
if "struct" == head[1]: self.addStruct(body) # struct element
else: # check if we are root or canvas node
if 7 == len(argv): last = self.addRoot(pd_lines=body)
elif 8 == len(argv): last = self.addCanvas(pd_lines=body)
elif "#A" == head[0]: #A -> text, savestate, or array data
super().__setdata__(last, Data(data=body, head=head[1]))
else: #X -----------------> anything else is an "#X"
if "declare" == head[1]: self.addDependencies(pd_lines=body)
elif "coords" == head[1]: self.addCoords(body)
elif "connect" == head[1]: self.addEdge(body) # edges
elif "floatatom" == head[1]: last = self.addNativeGui(head[1], body)
elif "symbolatom" == head[1]: last = self.addNativeGui(head[1], body)
elif "listbox" == head[1]: last = self.addNativeGui(head[1], body)
elif "scalar" == head[1]: last = self.addScalar(body)
elif "text" == head[1]: last = self.addComment(body)
elif "msg" == head[1]: last = self.addMsg(body)
elif "obj" == head[1]: last = self.addObj(body)
elif "pop" == head[1]: store_graph = False # pop the array old
elif "f" == head[1]: setattr(last, "border", int(body[0]))
elif "graph" == head[1]:
last = self.addGraph(body)
store_graph = True
elif "array" == head[1]: # fill; check if we are in a graph
if store_graph: last.addArray(head[1], *body)
else: last = self.addGOPArray(body)
elif "restore" == head[1]: #TODO do something to graph restore...?
if "graph" == body[-1]: last = self.restore(body)
else: last = self.restore(body)
else: log(1,"What is this?", argv, self.patchname)
def __pd__(self):
""" Unparse this instance's scope into a list of pure data argument vectors
This method returns a list of pure data argument vectors (1) from this
class' scope.
"""
s = ''
for x in getattr(self,'structs', []):
s += x.__pd__()
s += self.root.__pd__()
if hasattr(self, 'dependencies'):
s += self.dependencies.__pd__()
return super().__render__(s)
def __xml__(self):
""" Return the XML Element for this object """
# root tag to which struct, 'root', and dependencies will be added
x = super().__element__(scope=self, attrib={
"encoding": self.encoding,
"xml:space": "preserve"
})
if hasattr(self, 'structs'):
structs = super().__element__(tag="structs")
for e in getattr(self,'structs', []):
super().__subelement__(structs, e.__xml__())
super().__subelement__(x, structs)
if hasattr(self, 'dependencies'):
super().__subelement__(x, self.dependencies.__xml__())
# make the 'root' tag to which all other elements will be added
root = self.root.__xml__(tag='root')
# add the 'root' tag to the xml root
super().__subelement__(x, root)
super().__xml_nodes__(root)
super().__xml_comments__(root)
if hasattr(self, 'coords'):
super().__subelement__(root, self.coords.__xml__())
super().__xml_edges__(root)
if hasattr(self, 'position'):
super().__subelement__(root, self.position.__xml__())
if hasattr(self, 'title'):
super().__subelement__(root, 'title', text=self.title)
return super().__tree__(x)
def __set_pd_path__(self, path_to_pd):
""" Attempt to locate the ``pd`` executable """
from sys import platform
from pathlib import Path
installed = False
# print("Found ", platform, " platform.")
if "darwin" in platform: # macos
pdpath = Path("/Applications")
bindir = "/Contents/Resources/bin/pd"
elif "win" in platform: # windoz
pdpath = Path("C:/Program Files (x86)")
bindir = "/usr/bin/pd"
else: # asume pd is out there
installed = True
pdbin = Path("/")
if path_to_pd is not None:
pdpath = Path(path_to_pd)
if installed:
pdbin += "pd"
else:
# print("Locating pd...")
apps = [p for p in sorted(pdpath.glob("Pd*.app"))]
if len(apps) >= 1:
appdir = apps[0]
else:
log(2, "Could not find pd in " + pdpath)
pdbin = Path(appdir.as_posix() + bindir)
if Path(pdbin).exists():
# print("Found pd at: ", pdbin.as_posix())
setattr(self, '__pdbin__', pdbin)
else:
log(2, "This pd binary does not exist: " + pdbin.as_posix())
[docs] def run(self):
""" Run Pd from the Pure Data binary """
from subprocess import run as __run__
if not hasattr(self, 'patchname'):
raise Exception("No patchname found")
if not hasattr(self, '__pdbin__'):
raise Exception("Pd binary not set.")
command = [
self.__pdbin__.as_posix(),
"-open",
self.patchname+".pd",
]
__run__(" ".join(command), shell=True, check=True)
def __enter__(self):
return self
def __exit__(self, ctx_type, ctx_value, ctx_traceback):
self.write()
[docs] def arrangement(self, choice=-1):
""" Sets the arranger function
Parameters
----------
choice: :class:`int`
The choices are numbered starting at 0. If negative or not one of the available choices, it defaults to ``arrange1b``
"""
if choice == 0:
# good, but fails on circular conections
from ..utilities.arrange import arrange as _do_arrange
elif choice == 1:
# this one fails
from ..utilities.arrange1 import arrange1 as _do_arrange
elif choice == 2:
# fails on dac~
from ..utilities.arrange1a import arrange1a as _do_arrange
elif choice == 3:
# max recursion depth error
from ..utilities.arrange2 import arrange2 as _do_arrange
elif choice == 5:
# max recursion depth error
from ..extra.arranger import Arranger as _do_arrange
else:
# this one is nice (fails on dac~)
from ..utilities.arrange1b import arrange1b as _do_arrange
def _recurse_arrange(node):
# in every child of ``node``, check if child is a parent
for child in getattr(node, 'nodes', []):
if hasattr(child, 'nodes'):
# it is a parent, so recurse
_recurse_arrange(child)
# finally, arrange
_do_arrange(node)
def _begin_arrange(scope):
if hasattr(scope, 'root'):
_recurse_arrange(scope.root)
self.__arrange__ = _begin_arrange