diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..a81c8ee121952cf06bfaf9ff9988edd8cded763c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,138 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
diff --git a/rocolib/__init__.py b/rocolib/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..dbf33078e5757ddceb2bb81e5d1cf075dcc73593
--- /dev/null
+++ b/rocolib/__init__.py
@@ -0,0 +1,19 @@
+"""
+rocolib.
+
+The Robot Compiler backend library
+"""
+
+from os.path import dirname
+from os.path import realpath
+from os.path import relpath
+
+
+__version__ = "0.1"
+__author__ = 'UCLA LEMUR'
+__credits__ = 'The Laboratory for Embedded Machines and Ubiquitous Robots'
+
+ROCOLIB_DIR = dirname(realpath(__file__))
+
+def rocopath(f):
+    return relpath(f, ROCOLIB_DIR)
diff --git a/rocolib/api/Function.py b/rocolib/api/Function.py
new file mode 100644
index 0000000000000000000000000000000000000000..8024cdb56f60588e33f29411bab2e65dc3c556c9
--- /dev/null
+++ b/rocolib/api/Function.py
@@ -0,0 +1,46 @@
+class Function:
+  def __init__(self, params, fnstring=None):
+    self.params = params
+    self.fnstring = fnstring or "x"
+
+  def toYamlObject(self):
+    if self.params is None:
+      return eval(obj)
+    elif self.fnstring == "x":
+      return {"parameter": self.params}
+    else:
+      return {"function": self.fnstring, "parameter": self.params}
+
+  def fromYamlObject(self, obj):
+    if isinstance(obj, dict):
+      self.params = obj["parameter"]
+      self.fnstring = obj.get("function", "x")
+    else:
+      self.params = None
+      self.fnstring = repr(obj)
+
+  def eval(self, parameterizable):
+    import rocolib.utils.numsym as np
+    from rocolib.utils.dimensions import getDim
+    function = eval("lambda x : " + self.fnstring, locals())
+    if isinstance(self.params, (list, tuple)):
+      output = function([parameterizable.getParameter(x) for x in self.params])
+    elif self.params:
+      output = function(parameterizable.getParameter(self.params))
+    else:
+      output = function(None)
+    return output
+
+class ConstantFunction(Function):
+  def __init__(self, value):
+    Function.__init__(self, None, repr(value))
+
+class IdentityFunction(Function):
+  def __init__(self, params):
+    Function.__init__(self, params, "x")
+
+class YamlFunction(Function):
+  def __init__(self, obj):
+    Function.__init__(self, None, None)
+    self.fromYamlObject(obj)
+
diff --git a/rocolib/api/Parameterized.py b/rocolib/api/Parameterized.py
new file mode 100644
index 0000000000000000000000000000000000000000..a510d8122a7a4b32397db87ba16628480f82704c
--- /dev/null
+++ b/rocolib/api/Parameterized.py
@@ -0,0 +1,176 @@
+from rocolib.utils.dimensions import isDim
+from rocolib.utils.numsym import Dummy
+
+
+PARAM_TYPES = {
+    "length": {
+        "valueType": "(float, int)",
+        "minValue": 0,
+        "units": "mm",
+    },
+    "angle": {
+        "valueType": "(float, int)",
+        "minValue": 0,
+        "maxValue": 360,
+        "units": "degrees",
+    },
+    "ratio": {
+        "valueType": "(float, int)",
+        "minValue": 0,
+        "maxValue": 1,
+    },
+    "count": {
+        "valueType": "int",
+        "minValue": 0,
+    },
+    "dimension": {
+        "valueType": "str",
+        #"isValid": isDim,
+    },
+}
+
+class Parameter:
+    def __init__(self, name, defaultValue=None, paramType=None, **kwargs):
+        ### XXX "." is used to separate subcomponent parameters when inherited
+        ###     but we can't tell the difference here, so it's not invalid I guess
+
+        #if "." in name:
+            #raise ValueError("Invalid character '.' in parameter name " + name)
+        self.name = name
+
+        if defaultValue is None and not kwargs.get("optional", False):
+            raise ValueError(f"Must specify either defaultValue or optional=True for parameter {name}")
+
+        self.defaultValue = defaultValue
+        self.spec = {}
+
+        if paramType in PARAM_TYPES:
+            for k, v in PARAM_TYPES[paramType].items():
+                self.spec[k] = v
+        elif paramType:
+            raise ValueError(f"Unknown paramType: {paramType}")
+        for k, v in kwargs.items():
+            self.spec[k] = v
+        self.assertValid(defaultValue)
+
+        vt = self.spec.get("valueType", "")
+        no = not(self.spec.get("optional", False))
+
+        if no and ("int" in vt or "float" in vt):
+            integer=None
+            positive=None
+
+            if "float" not in vt:
+                integer=True
+
+            mv = self.spec.get("minValue", -1)
+            if mv is not None and mv >= 0:
+                positive=True
+
+            self.symbol = Dummy(name, real=True, positive=positive, integer=integer)
+        else:
+            self.symbol = None
+        self.value = None
+
+    def setValue(self, value):
+        self.assertValid(value)
+        self.value = value
+
+    def setDefault(self, force=False):
+        if force or self.value is None:
+            self.setValue(self.defaultValue)
+
+    def assertValid(self, value):
+        if value is None:
+            if self.spec.get("optional", False):
+                self.value = value
+                return
+            else:
+                raise ValueError(f"Parameter {self.name} is not optional and cannot be set to None")
+
+        def check(spec, test, error):
+            if (self.spec.get(spec, None) is not None and not test(self.spec[spec])):
+                raise ValueError(f"When setting parameter {self.name}: {value} {error} {self.spec[spec]}")
+
+        check("valueType", lambda x : isinstance(value, eval(x)), "is not of type")
+        check("minValue", lambda x : value >= x, "is less than")
+        check("maxValue", lambda x : value <= x, "is greater than")
+        check("isValid", lambda x : x(value), "is invalid")
+        return True
+
+    def getValue(self):
+        if self.value is not None:
+            return self.value
+        else:
+            return self.symbol
+
+    def getSpec(self):
+        return self.spec
+
+class Parameterized(object):
+    """
+    Like a dictionary k/v store, but we require special syntax constructs
+    to set/update keys
+
+    XXX FIX: Name is duplicated both in the Parameter object and the dict key
+    """
+    def __init__(self):
+        self.parameters = {}
+
+
+    def addParameter(self, name, defaultValue=None, **kwargs):
+        """
+        Adds a k/v pair to the internal store if the key has not been added before
+        Raises KeyError if the key has been added before
+        """
+        if name in self.parameters:
+            raise KeyError("Parameter %s already exists on object %s" % (name, str(self)))
+        p = Parameter(name, defaultValue, **kwargs)
+        self.parameters.setdefault(name, p)
+        return p
+
+
+    def useDefaultParameters(self, force=False):
+        """
+        Assign the default value to all parameters:
+        - Only if otherwise unset if force=False
+        - Overwrite any previously set values if force=True
+        """
+        for n, p in self.parameters.items():
+            p.setDefault(force)
+
+
+    def setParameter(self, n, v):
+        """
+        Sets a k/v pair to the internal store if the key has been added previously
+        Raises KeyError if the key has not been added before
+        Passes along any ValueErrors from Parameter object
+        """
+        if n in self.parameters:
+            self.parameters[n].setValue(v)
+        else:
+            raise KeyError("Parameter %s not initialized on object %s" % (n, str(self)))
+
+
+    def getParameter(self, name):
+        """
+        Retrieves the parameter value with the given name
+        Raises KeyError if the key is not been set
+        """
+        return self.parameters[name].getValue()
+
+
+    def getParameterInfo(self):
+        """
+        Retrieves the parameter metadata info
+        """
+        return {k: v.__dict__ for k, v in self.parameters.items()}
+
+
+    def hasParameter(self, name):
+        return name in self.parameters
+
+
+    def delParameter(self, name):
+        self.parameters.pop(name)
+
diff --git a/rocolib/api/__init__.py b/rocolib/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c376a9d3ed20473396ff70a5765ada32d2fad412
--- /dev/null
+++ b/rocolib/api/__init__.py
@@ -0,0 +1,3 @@
+
+#from CodeComponent import CodeComponent
+#from UIComponent import UIComponent
\ No newline at end of file
diff --git a/rocolib/api/components/Component.py b/rocolib/api/components/Component.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcf1e9b7ae37d374d1542c28b3a8bcd27d19fcca
--- /dev/null
+++ b/rocolib/api/components/Component.py
@@ -0,0 +1,585 @@
+from collections import OrderedDict
+import os
+import sys
+import yaml
+import logging
+import networkx as nx
+
+from rocolib import ROCOLIB_DIR
+from rocolib.api.Parameterized import Parameterized
+from rocolib.api.Function import YamlFunction
+from rocolib.utils.utils import prefix as prefixString
+from rocolib.utils.utils import tryImport
+from rocolib.utils.io import load_yaml
+
+
+log = logging.getLogger(__name__)
+# XXX circular import: 
+# from rocolib.library import ROCOLIB_LIBRARY
+# instead: 
+ROCOLIB_LIBRARY = os.path.join(ROCOLIB_DIR, "library")
+
+def getSubcomponentObject(component, name=None):
+    try:
+        obj = tryImport(component, component)
+        # XXX hack to get around derived components not having name parameter in their __init__
+        c = obj()
+        c.setName(name)
+        return c
+    except ImportError:
+        c = Component(component)
+        c.setName(name)
+        return c
+
+class Component(Parameterized):
+    @classmethod
+    def test(cls, params=None, **kwargs):
+        c = cls()
+        if params:
+            for key, val in params.items():
+                c.setParameter(key, val)
+        filedir = kwargs.pop('filedir', None)
+        c.makeOutput(filedir, **kwargs)
+
+    def __init__(self, yamlFile=None):
+        self._name = None
+        Parameterized.__init__(self)
+        self.reset()
+        yf = yamlFile
+        if not yamlFile:
+            yf = type(self).__name__ + ".yaml"
+        for fn in (yf, 
+                   os.path.join(ROCOLIB_DIR, yf), 
+                   os.path.join(ROCOLIB_LIBRARY, yf)):
+            try:
+                self.fromYaml(fn)
+                break
+            except IOError:
+                pass
+            if yamlFile:
+                raise ValueError(f"No suitable yamlfile found for {yamlFile}")
+        self.predefine()
+        self.define()
+
+    def getName(self):
+      return self._name if self._name is not None else str(self.__class__)
+
+    def setName(self, name):
+      self._name = name
+
+    def fromYaml(self, filename):
+        definition = load_yaml(filename)
+
+        # keys are (parameters, subcomponents, connections, interfaces)
+        if "parameters" in definition:
+          for k, v in definition["parameters"].items():
+              self.addParameter(k, v["defaultValue"], **v["spec"])
+
+        if "subcomponents" in definition:
+          val = definition["subcomponents"]
+          for k, v in val.items():
+            self.addSubcomponent(k, v["classname"], **v["kwargs"])
+            self.subcomponents[k]["parameters"] = v["parameters"]
+
+        if "connections" in definition:
+          self.connections = definition["connections"]
+
+        if "interfaces" in definition:
+          val = definition["interfaces"]
+          for k, v in val.items():
+            if isinstance(v, dict):
+              self.inheritInterface(k, (v["subcomponent"], v["interface"]))
+            else:
+              self.interfaces[k] = v
+          self.reinheritAllInterfaces()
+
+    def reset(self):
+        # Used during design
+        self.parameters = {}
+        self.subcomponents = {}
+        self.connections = {}
+        self.interfaces = {}
+        self.defaults = {}
+        self.semanticConstraints = []
+
+        # Used during make
+        self.composables = OrderedDict()
+
+    def predefine(self, **kwargs):
+        ### Override in Component subclass to define subclass parameters, interfaces, etc.
+        pass
+
+    def define(self):
+        ### Override in Component instance to define individual parameters, interfaces, etc.
+        pass
+
+    ###
+    # DESIGN PHASE
+    ###
+
+    def addSubcomponent(self, name, classname, inherit=False, prefix = "", **kwargs):
+        '''
+
+        :param name: unique identifier to refer to this component by
+        :type  name: str or unicode
+        :param classname: code name of the subcomponent
+                    should be python file/class or yaml name
+        :type  classname: str or unicode
+        '''
+        # XXX will silently fail if subcomponent name is already taken?
+        obj = getSubcomponentObject(classname, self.getName() + '.' + name)
+        sc = {"classname": classname, "parameters": {}, "kwargs": kwargs, "object": obj}
+        self.subcomponents.setdefault(name, sc)
+
+        if inherit:
+            if prefix == "":
+                prefix = name
+
+            for key, value in obj.parameters.items():
+                # inherit = True : inherit all parameters
+                if inherit is True or key in inherit:
+                    try:
+                        self.addParameter(prefixString(prefix, key), value.defaultValue, **value.spec)
+                    except KeyError:
+                        # It's ok if we try to add a parameter that already exists
+                        pass
+                    self.addConstraint((name, key), prefixString(prefix, key))
+            # XXX also inherit interfaces?
+
+        return self
+
+    def delSubcomponent(self, name):
+        toDelete = []
+
+        # delete edges connecting components
+        for connName, ((fromComp, _), (toComp, _), _) in self.connections.items():
+            if name in (fromComp, toComp):
+                toDelete.append(connName)
+        for connName in toDelete:
+            self.connections.pop(connName)
+
+        self.subcomponents.pop(name)
+
+    def addConstraint(self, constraint_params, inputs, function=None, subcomponent=None):
+        data = {"parameter": inputs}
+        if subcomponent:
+            data["subcomponent"] = subcomponent
+        if function:
+            data["function"] = function
+
+        self.addConstConstraint(constraint_params, data)
+
+    def addConstConstraint(self, constraint_params, value):
+        (subComponent, parameterName) = constraint_params
+
+        # If constraint exists and was inherited
+        # i.e. is the constant function for prefixstring'ed 
+        # then remove the inherited parameter 
+        newkey = prefixString(subComponent, parameterName)
+        try:
+            if self.hasParameter(newkey) \
+                and self.getSubParameters(subComponent).get(parameterName).get("parameter") == newkey \
+                and self.getSubParameters(subComponent).get(parameterName).get("function") is None:
+                    if self.getParameterInfo()[newkey]["spec"].get("optional"):
+                        for override in self.getParameterInfo()[newkey]["spec"].get("overrides", ()):
+                            self.delParameter(prefixString(subComponent, override))
+                            self.delConstraint(subComponent, override)
+                    self.delParameter(newkey)
+        except AttributeError as e:
+            pass
+
+        # XXX otherwise silently overwrites existing constraints, is that ok?
+        self.getSubParameters(subComponent)[parameterName] = value
+
+    def delConstraint(self, subComponent, parameterName):
+        self.getSubParameters(subComponent).pop(parameterName)
+
+    def addInterface(self, name, val):
+        if name in self.interfaces:
+            raise ValueError("Interface %s already exists" % name)
+        self.interfaces.setdefault(name, val)
+        return self
+
+    def inheritAllInterfaces(self, subcomponent, prefix=""):
+        obj = self.subcomponents[subcomponent]["object"]
+        if prefix == "":
+          prefix = subcomponent
+        for name, value in obj.interfaces.items():
+          self.inheritInterface(prefixString(prefix, name), (subcomponent, name))
+        self.reinheritAllInterfaces()
+        return self
+
+    def inheritInterface(self, name, interface_params):
+        (subcomponent, subname) = interface_params
+        if name in self.interfaces:
+            raise ValueError("Interface %s already exists" % name)
+        value = {"subcomponent": subcomponent, "interface": subname}
+        obj = self.subcomponents[subcomponent]["object"]
+        iobj = obj.getInterface(subname, transient=True).inherit(self, subcomponent)
+        if iobj:
+          value["object"] = iobj
+        self.interfaces.setdefault(name, value)
+
+        return self
+
+    # XXX kinda hackish?
+    def reinheritAllInterfaces(self):
+      for name, value in self.interfaces.items():
+        if isinstance(value, dict):
+          subc = value["subcomponent"]
+          subi = value["interface"]
+          obj = self.subcomponents[subc]["object"]
+          iobj = obj.getInterface(subi, transient=True).inherit(self, subc)
+          if iobj:
+            value["object"] = iobj
+          self.interfaces.setdefault(name, value)
+
+    # Create both constraints and connections
+    def join(self, fromInterface, toInterface, name=None, **kwargs):
+        fromComponent = fromInterface[0]
+        fromPort = self.getInterfaces(*fromInterface)
+        toComponent = toInterface[0]
+        toPort = self.getInterfaces(*toInterface)
+        for fromParam, toParam in zip(fromPort.getParams(), toPort.getParams()):
+            self.addConstraint((toComponent, toParam), fromParam, subcomponent=fromComponent)
+        self.addConnection(fromInterface, toInterface, name, **kwargs)
+
+    def addConnection(self, fromInterface, toInterface, name=None, **kwargs):
+        if name is None:
+          for i in range(len(self.connections)+1):
+            name = "connection%d" % i
+            if name not in self.connections:
+              break
+        for k, v in kwargs.items():
+          try:
+            kwargs[k] = v.toYamlObject()
+          except AttributeError:
+            pass
+
+        fromPort = self.getInterfaces(*fromInterface)
+        toPort = self.getInterfaces(*toInterface)
+        if fromPort.canMate(toPort):
+            self.connections.setdefault(name, [fromInterface, toInterface, kwargs])
+        else:
+            raise AttributeError(f"{fromInterface} cannot connect to {toInterface} according to getMate")
+
+    def delConnection(self, name):
+      self.connections.pop(name)
+
+    def getConnections(self, component1, component2=None):
+      keys = []
+      for connName, (fromInterface, toInterface, kwargs) in self.connections.items():
+        if component1 in [fromInterface[0], toInterface[0]]:
+          if component2 is None or component2 in [fromInterface[0], toInterface[0]]:
+            keys.append(connName)
+      return keys
+          
+
+    '''
+    # TODO : delete Interface
+    # XXX : remove constraints that involve this parameter?
+    def delParameter(self, name):
+    '''
+
+    def toLibrary(self, name):
+        # XXX TODO: Check for collisions!  
+        # if collision:
+        #   flag to allow rebuilding, and fail otherwise?  
+        #   if no flag, check if source matches and rebuild if so, fail otherwise?
+        return self.toYaml(ROCOLIB_LIBRARY, name + ".yaml")
+
+    def toYaml(self, basedir, filename):
+        filepath = os.path.join(basedir, filename)
+        source = os.path.relpath(sys.argv[0], basedir)
+
+        parameters = {}
+        for k, v in self.parameters.items():
+          parameters[k] = {"defaultValue": v.defaultValue, "spec": v.spec}
+
+        subcomponents = {}
+        for k, v in self.subcomponents.items():
+          subcomponents[k] = {"classname": v["classname"], "parameters": v["parameters"], "kwargs": v["kwargs"]}
+
+        interfaces = {}
+        for k, v in self.interfaces.items():
+          if isinstance(v, dict):
+            interfaces[k] = {"subcomponent": v["subcomponent"], "interface": v["interface"]}
+          else:
+            interfaces[k] = v
+
+        definition = {
+            "source" : source,
+            "parameters" : parameters,
+            "subcomponents" : subcomponents,
+            "connections" : self.connections,
+            "interfaces" : interfaces,
+        }
+
+        with open(filepath, "w") as fd:
+            yaml.safe_dump(definition, fd)
+
+    ###
+    # GETTERS AND SETTERS
+    ###
+
+    def getSubcomponent(self, name):
+        return self.subcomponents[name]['object']
+
+    def getSubParameters(self, name):
+        return self.subcomponents[name]['parameters']
+
+    def setSubParameter(self, c, n, v):
+        self.getSubcomponent(c).setParameter(n, v)
+
+    def getInterfaces(self, component, name, transient=False):
+        return self.getSubcomponent(component).getInterface(name, transient)
+
+    def getInterface(self, name, transient=False):
+        c = self.interfaces[name]
+
+        if isinstance(c, dict):
+            if "object" in c and c["object"]: 
+              return c["object"]
+            
+            subc = c["subcomponent"]
+            subi = c["interface"]
+            i = self.getInterfaces(subc, subi, transient)
+            if not transient:
+              c["object"] = i
+            return i
+        else:
+          return c
+
+    def setInterface(self, n, v):
+        if n in self.interfaces:
+            self.interfaces[n] = v
+        else:
+            raise KeyError("Interface %s not initialized" % n)
+        return self
+
+    ###
+    # ASSEMBLY PHASE
+    ###
+
+    def assemble(self):
+        ### Override to combine components' drawings to final drawing
+        pass
+
+    def append(self, name, prefix, **kwargs):
+        component = self.getSubcomponent(name)
+
+        allPorts = set()
+        for key in component.interfaces:
+          try:
+            allPorts.update(component.getInterface(key))
+          except TypeError:
+            # interface is not iterable, i.e. a single port
+            allPorts.add(component.getInterface(key))
+        for port in allPorts:
+          port.prefix(prefix)
+
+        for (key, composable) in component.composables.items():
+            self.composables[key].append(composable, prefix, **kwargs)
+
+    def attach(self, interface_1, interface_2, kwargs):
+        (fromName, fromPort) = interface_1
+        (toName, toPort) = interface_2
+        for (key, composable) in self.composables.items():
+            try:
+                composable.attachInterfaces(self.getInterfaces(fromName, fromPort),
+                                            self.getInterfaces(toName, toPort),
+                                            kwargs)
+            except:
+                logstr  =  "Error in attach: \n"
+                logstr += f"  from ({fromName}, {fromPort}): "
+                logstr += self.getInterfaces(fromName, fromPort).toString()
+                logstr +=  "\n"
+                logstr += f"  to ({toName}, {toPort}): "
+                logstr += self.getInterfaces(toName, toPort).toString()
+                log.error(logstr)
+                raise
+
+    ###
+    # BUILD PHASE
+    ###
+
+    def modifyParameters(self):
+        # Override to manually specify how parameters get set during build
+        pass
+
+    def traverseGraph(self):
+        graph = nx.DiGraph()
+        for ((fromComponent, fromPort), (toComponent, toPort), kwargs) in list(self.connections.values()):
+            graph.add_edge(fromComponent, toComponent)
+        try:
+            nx.find_cycle(graph, orientation='original')
+        except nx.NetworkXNoCycle:
+            pass
+        else:
+            log.warning("Cycle found in subcomponent connection graph, behavior is currently unspecified")
+        return list(nx.topological_sort(graph))
+
+    def evalConstraints(self, subComponent):
+        for (parameterName, obj) in self.getSubParameters(subComponent).items():
+            try:
+                parent = obj.get("subcomponent")
+                if not parent:
+                    raise AttributeError
+                else:
+                    parent = self.getSubcomponent(parent)
+            except AttributeError:
+                parent = self
+
+            try:
+              x = YamlFunction(obj).eval(parent)
+            except Exception as e:
+              log.error(f"Error trying to evaluate constraints for {parameterName} on {obj}:")
+              log.error(repr(e))
+              raise
+
+            if x is not None:
+              self.setSubParameter(subComponent, parameterName, x)
+
+    # Append composables from all known subcomponents
+    # (including ones without explicitly defined connections)
+    # set useDefaultParameters = False to replace unset parameters with sympy variables
+    def evalComponent(self, name, useDefaultParameters=True):
+        sc = self.subcomponents[name]
+        obj = sc['object']
+        classname = sc['classname']
+        try: 
+          kwargs = sc['kwargs']
+        except IndexError:
+          kwargs = {}
+
+        try:
+            obj.make(useDefaultParameters)
+            for (key, composable) in obj.composables.items():
+                if key not in self.composables:
+                    self.composables[key] = composable.new()
+            self.append(name, name, **kwargs)
+        except:
+            log.error("Error in subclass %s, instance %s" % (classname, name))
+            raise
+
+    def evalConnections(self, scName):
+        for ((fromComponent, fromPort), (toComponent, toPort), kwargs) in self.connections.values():
+            if toComponent != scName:
+                continue
+            for k, v in kwargs.items():
+                kwargs[k] = YamlFunction(v).eval(self)
+            self.attach((fromComponent, fromPort),
+                        (toComponent, toPort),
+                        kwargs)
+
+    def informComposables(self):
+      # Let composables know what components and interfaces exist
+      # TODO remove this when we have a better way of letting composables
+      # know about components that have no ports (ex Bluetooth module driver)
+      for (key, composable) in self.composables.items():
+        for (name, sc) in self.subcomponents.items():
+          composable.addComponent(sc['object'])
+        for (name, value) in self.interfaces.items():
+          if value is not None:
+            composable.addInterface(self.getInterface(name))
+
+    # set useDefaultParameters = False to replace unset parameters with sympy variables
+    def make(self, useDefaultParameters=True):
+        if useDefaultParameters:
+            self.useDefaultParameters()
+        self.modifyParameters()
+
+        scOrder = self.traverseGraph()
+        unconnected = list(set(self.subcomponents.keys()) - set(scOrder))
+        for scName in scOrder + unconnected:
+            self.evalConstraints(scName)
+            self.evalComponent(scName, useDefaultParameters)    # Merge composables from all subcomponents and tell them my components exist
+            self.evalConnections(scName)   # Tell composables which interfaces are connected
+
+        self.informComposables()    # Tell composables that my interfaces exist
+        self.assemble()
+
+    ###
+    # OUTPUT PHASE
+    ###
+
+    def makeComponentHierarchy(self):
+        hierarchy = {}
+        for n, sc in self.subcomponents.items():
+            sub = sc['object']
+            c = sc['classname']
+            hierarchy[n] = {"class":c, "subtree":sub.makeComponentHierarchy()}
+        return hierarchy
+
+    def makeComponentTree(self, basename, stub, root="Root"):
+        import pydot
+        graph = pydot.Dot(graph_type='graph')
+        mynode = pydot.Node(root, label = root)
+        self.recurseComponentTree(graph, mynode, root)
+        if basename:
+            graph.write_png(basename+stub)
+            ret = basename+stub
+        else:
+            ret = graph.to_string()
+        return ret
+
+    def recurseComponentTree(self, graph, mynode, myname):
+        import pydot
+        for n, sc in self.subcomponents.items():
+            sub = sc['object']
+            c = sc['classname']
+            fullstr = myname + "/" + n
+            subnode = pydot.Node(fullstr, label = c + r"\n" + n)
+            graph.add_node(subnode)
+            edge = pydot.Edge(mynode, subnode)
+            graph.add_edge(edge)
+            sub.recurseComponentTree(graph, subnode, fullstr)
+
+    def makeOutput(self, filedir=".", **kwargs):
+        log.info(f"Compiling robot designs to directory {filedir} ...")
+        def kw(arg, default=kwargs.get("default", True)):
+            return kwargs.get(arg, default)
+
+        if kw("remake", True):
+            self.make(kw("useDefaultParameters", True))
+            log.debug(f"... done making {self.getName()}.")
+
+        # XXX: Is this the right way to do it?
+        import os
+        try:
+            os.makedirs(filedir)
+        except:
+            pass
+
+        # Process composables in some ordering based on type
+        orderedTypes = ['electrical', 'ui', 'code'] # 'code' needs to know about pins chosen by 'electrical', and 'code' needs to know about IDs assigned by 'ui'
+        # First call makeOutput on the ones of a type whose order is specified
+        rets = {}
+        for composableType in orderedTypes:
+            if composableType in self.composables:
+                kwargs["name"] = composableType
+                ss = self.composables[composableType].makeOutput(filedir, **kwargs)
+                rets[composableType] = ss
+        # Now call makeOutput on the ones whose type did not care about order
+        for (composableType, composable) in self.composables.items():
+            if composableType not in orderedTypes:
+                kwargs["name"] = composableType
+                ss = self.composables[composableType].makeOutput(filedir, **kwargs)
+                rets[composableType] = ss
+
+        if kw("tree"):
+            log.info("Generating hierarchy tree...")
+            rets["component"] = self.makeComponentTree(filedir, "/tree.png")
+            log.debug("done making tree.")
+
+        log.info("Happy roboting!")
+        return rets
+
+    ###
+    # OTHER STUFF
+    # (probably obsolete)
+    ###
+
+    def addSemanticConstraint(self, lhs, op, rhs):
+        self.semanticConstraints.append([lhs, op, rhs])
+
diff --git a/rocolib/api/components/DecorationComponent.py b/rocolib/api/components/DecorationComponent.py
new file mode 100644
index 0000000000000000000000000000000000000000..2826d8b9bd57f29a52ec6e6e45ae7867b2bbd378
--- /dev/null
+++ b/rocolib/api/components/DecorationComponent.py
@@ -0,0 +1,15 @@
+from rocolib.api.components import MechanicalComponent
+from rocolib.api.composables.GraphComposable import DecorationComposable
+from rocolib.api.ports import MountPort
+
+
+class DecorationComponent(MechanicalComponent):
+  def predefine(self, **kwargs):
+    MechanicalComponent.predefine(self, **kwargs)
+
+    self.graph = DecorationComposable()
+    self.addFace = self.graph.addFace
+    self.addInterface("decoration", MountPort(self, self.graph))
+
+  def getGraph(self):
+    return self.graph
diff --git a/rocolib/api/components/FoldedComponent.py b/rocolib/api/components/FoldedComponent.py
new file mode 100644
index 0000000000000000000000000000000000000000..e41f3401c45eab3d1e4e229aedd67b146696951f
--- /dev/null
+++ b/rocolib/api/components/FoldedComponent.py
@@ -0,0 +1,39 @@
+from rocolib.api.components import MechanicalComponent
+from rocolib.api.composables import GraphComposable
+from rocolib.api.ports import EdgePort
+from rocolib.api.ports import FacePort
+
+
+class FoldedComponent(MechanicalComponent):
+  GRAPH = 'graph'
+
+  def predefine(self, **kwargs):
+    MechanicalComponent.predefine(self, **kwargs)
+
+    g = GraphComposable()
+    self.composables[self.GRAPH] = g
+
+    self.place = self.getGraph().place
+    self.mergeEdge = self.getGraph().mergeEdge
+    self.addTab = self.getGraph().addTab
+    self.getEdge = self.getGraph().getEdge
+    self.attachEdge = self.getGraph().attachEdge
+    self.addFace = self.getGraph().addFace
+    self.attachFace = self.getGraph().attachFace
+
+  def getGraph(self):
+    return self.composables[self.GRAPH]
+
+  def addEdgeInterface(self, interface, edges, lengths):
+    self.addInterface(interface, None)
+    self.setEdgeInterface(interface, edges, lengths)
+
+  def addFaceInterface(self, interface, face):
+    self.addInterface(interface, None)
+    self.setFaceInterface(interface, face)
+
+  def setEdgeInterface(self, interface, edges, lengths):
+    self.setInterface(interface, EdgePort(self, edges, lengths))
+
+  def setFaceInterface(self, interface, face):
+    self.setInterface(interface, FacePort(self, self.getGraph(), face))
diff --git a/rocolib/api/components/MechanicalComponent.py b/rocolib/api/components/MechanicalComponent.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b1bda83ca9590c054044390318b12bc6b4d387f
--- /dev/null
+++ b/rocolib/api/components/MechanicalComponent.py
@@ -0,0 +1,48 @@
+import logging
+
+from rocolib.api.components import Component
+from rocolib.api.ports import AnchorPort
+from rocolib.utils.transforms import np, Transform6DOF
+
+
+log = logging.getLogger(__name__)
+
+def vals(x):
+    if x is None:
+        return None
+    return list(map(lambda p: p.getValue(), x))
+
+class MechanicalComponent(Component):
+    def predefine(self, **kwargs):
+        self._origin = [ self.addParameter("_d"+x, 0, paramType="length", minValue=None) 
+            for x in "xyz"
+        ]
+
+        if kwargs.get("euler", False):
+            self._euler = [ self.addParameter(x, 0, paramType="angle") 
+                for x in "_roll _pitch _yaw".split()
+            ]
+        else:
+            self._euler = None
+            self._quat = [ self.addParameter("_q_"+x, int(x == "a"), 
+                                valueType="(int, float)", minValue=-1, maxValue=1) 
+                for x in "aijk"
+            ]
+            #self.addSemanticConstraint(np.Eq(np.norm(self._quat), 1))
+
+        #self.addInterface("", TPort())
+
+    def get6DOF(self):
+        return Transform6DOF(vals(self._origin), vals(self._euler), vals(self._quat))
+
+    def makeOutput(self, *args, **kwargs):
+        ## XXX Duplicated from component to set parameters for transform
+        if kwargs.get("remake", True):
+            self.make(kwargs.get("useDefaultParameters", True))
+            log.debug(f"... done making {self.getName()}.")
+            kwargs["remake"] = False
+
+        if "transform3D" not in kwargs:
+            kwargs["transform3D"] = self.get6DOF()
+
+        return Component.makeOutput(self, *args, **kwargs)
diff --git a/rocolib/api/components/__init__.py b/rocolib/api/components/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5aaf01598effac1c438892ac83b274102e6d30f
--- /dev/null
+++ b/rocolib/api/components/__init__.py
@@ -0,0 +1,4 @@
+from .Component import Component
+from .MechanicalComponent import MechanicalComponent
+from .FoldedComponent import FoldedComponent
+from .DecorationComponent import DecorationComponent
diff --git a/rocolib/api/composables/Composable.py b/rocolib/api/composables/Composable.py
new file mode 100644
index 0000000000000000000000000000000000000000..887fb99c2695735773d6c9ce6ffa7d97fc95100b
--- /dev/null
+++ b/rocolib/api/composables/Composable.py
@@ -0,0 +1,26 @@
+class Composable:
+  def new(self):
+    return self.__class__()
+  def append(self, newComposable, newPrefix, **kwargs):
+    raise NotImplementedError
+  def addComponent(self, componentObj):
+    pass
+  def addInterface(self, newInterface):
+    pass
+
+  def attachInterfaces(self, interface1, interface2, kwargs):
+    # Interfaces can contain multiple ports, so try each pair of ports
+    if not isinstance(interface1, (list, tuple)):
+      interface1 = [interface1]
+    if not isinstance(interface2, (list, tuple)):
+      interface2 = [interface2]
+    if len(interface1) != len(interface2):
+      raise AttributeError("Number of ports in each interface don't match")
+
+    for (port1, port2) in zip(interface1, interface2):
+      self.attach(port1, port2, kwargs)
+
+  def attach(self, fromPort, toPort, kwargs):
+    raise NotImplementedError
+  def makeOutput(self, filedir, **kwargs):
+    raise NotImplementedError
diff --git a/rocolib/api/composables/GraphComposable.py b/rocolib/api/composables/GraphComposable.py
new file mode 100644
index 0000000000000000000000000000000000000000..29818bdca6a7ddf5eb9c6e83524588dd4f7a6ea1
--- /dev/null
+++ b/rocolib/api/composables/GraphComposable.py
@@ -0,0 +1,181 @@
+import sys
+import logging
+from io import StringIO, BytesIO
+
+from rocolib.api.composables.graph.Graph import Graph as BaseGraph
+from rocolib.api.composables.graph.Drawing import Drawing
+from rocolib.api.composables.Composable import Composable
+from rocolib.utils.utils import decorateGraph
+from rocolib.utils.tabs import BeamTabs, BeamTabDecoration, BeamSlotDecoration
+import rocolib.utils.numsym as np
+
+
+log = logging.getLogger(__name__)
+
+class DecorationComposable(Composable, BaseGraph):
+  def __init__(self):
+    BaseGraph.__init__(self)
+  def append(self, newComposable, newPrefix, **kwargs):
+    pass
+  def attach(self, fromInterface, toInterface, kwargs):
+    pass
+  def makeOutput(self, filedir, **kwargs):
+    pass
+
+class GraphComposable(Composable, BaseGraph):
+  def __init__(self):
+    BaseGraph.__init__(self)
+
+  def append(self, g2, prefix2, **kwargs):
+    if kwargs.get("invert", False):
+        g2.invertEdges()
+    g2.prefix(prefix2)
+
+    if kwargs.get("root", False):
+        self.faces = g2.faces + self.faces
+        self.edges = g2.edges + self.edges
+    else:
+        self.faces.extend(g2.faces)
+        self.edges.extend(g2.edges)
+
+  def attach(self, port1, port2, kwargs):
+    # Test whether ports are of right type --
+    # Attach if both ports contain edges to attach along
+    try:
+      label1 = port1.getEdges()
+      label2 = port2.getEdges()
+    except AttributeError:
+      pass
+    else:
+      # XXX associate ports with specific composables so this isn't necessary
+      for i in range(len(label1)):
+        if label1[i] not in (e.name for e in self.edges):
+          return
+        if label2[i] not in (e.name for e in self.edges):
+          return
+
+      for i in range(len(label1)):
+        newargs = {}
+        for key, value in kwargs.items():
+          if isinstance(value, (list, tuple)):
+            newargs[key] = value[i]
+          else:
+            newargs[key] = value
+        self.mergeEdge(label1[i], label2[i], **newargs)
+
+    # If the first port contains a Face and the second contains a Decoration:
+    #     Decorate the face with the decoration
+    try:
+      face = self.getFace(port1.getFaceName())
+      deco = port2.getDecoration()
+    except AttributeError:
+      pass
+    else:
+      # XXX associate ports with specific composables so this isn't necessary
+      if face is not None:
+        decorateGraph(face, decoration=deco, **kwargs)
+
+    # If the first port contains a Decoration and the second contains a Face
+    #     Attach a face to the decoration's face
+    try:
+      deco = port1.getDecoration().faces[0]
+      face = port2.getFaceName()
+    except AttributeError:
+      pass
+    else:
+      self.mergeFace(deco.joinedFaces[0][0].name, face, np.dot(port2.getTransform(), deco.transform2D))
+
+    # If the first port contains a Face and the second contains a Face
+    #     Attach the two faces
+    try:
+      face1 = self.getFace(port1.getFaceName())
+      face2 = self.getFace(port2.getFaceName())
+    except AttributeError:
+      pass
+    else:
+      self.mergeFace(face1.name, face2.name, np.eye(4))
+
+  def makeOutput(self, filedir, **kwargs):
+    if "displayOnly" in kwargs:
+      kwDefault = not kwargs["displayOnly"]
+      kwargs["display"] = kwargs["displayOnly"]
+    elif "default" in kwargs:
+      kwDefault = kwargs["default"]
+    else:
+      kwDefault = True
+
+    def kw(arg, default=kwDefault):
+      if arg in kwargs:
+        return kwargs[arg]
+      return default
+
+    self.tabify(kw("tabFace", BeamTabs), kw("tabDecoration", BeamTabDecoration),
+                kw("slotFace", None), kw("slotDecoration", BeamSlotDecoration), **kwargs)
+    if kw("joint", None):
+        self.jointify(**kwargs)
+    self.place(transform3D=kw("transform3D", None))
+
+    basename = None
+    if filedir:
+        basename = filedir + "/" + kw("name", "") + "-"
+
+    rets = {}
+    bufs = {}
+
+    def handle(keyword, text, fn, stub, **ka):
+        if kw(keyword):
+          log.info("Generating %s pattern..." % text)
+          sys.stdout.flush()
+          if basename and stub:
+            with open(basename+stub, 'w') as fp:
+              fn(fp, **ka)
+          else:
+            buf = StringIO()
+            fn(buf, **ka)
+            rets[keyword] = buf.getvalue()
+            bufs[keyword] = buf
+            
+          log.debug("Done generating %s pattern." % text)
+
+    d = Drawing()
+    if kw("display", False) or kw("unfolding") or kw("autofolding") or kw("silhouette") or kw("animate"):
+      d.fromGraph(self)
+      d.transform(relative=(0,0))
+
+    handle("unfolding", "Corel cut-and-fold", d.toSVG, "lasercutter.svg", mode="Corel")
+    handle("unfolding", "printer", d.toSVG, "lasercutter.svg", mode="print")
+    handle("animate", "OrigamiSimulator", d.toSVG, "anim.svg", mode="animate")
+    handle("silhouette", "Silhouette cut-and-fold", d.toDXF, "silhouette.dxf", mode="silhouette")
+    handle("autofolding", "autofolding", d.toDXF, "autofold-default.dxf", mode="autofold")
+    handle("autofolding", "  -- (graph)", d.toDXF, "autofold-graph.dxf")
+    handle("stl", "3D", self.toSTL, "model.stl", **kwargs)
+
+    if kw("display3D", False) or kw("png"):
+      from rocolib.utils.display import display3D
+
+      if basename:
+        if kw("stl"):
+          stl_handle = open(basename+"model.stl", 'rb') 
+        else:
+          log.info("STL wasn't selected, making now...")
+          handle("png", "3D", self.toSTL, None, **kwargs)
+          stl_handle = bufs['png']
+          stl_handle.seek(0)
+        if kw("png"):
+          png_file = basename+"model.png"
+      else:
+        stl_handle = bufs['stl']
+        stl_handle.seek(0)
+        png_file = BytesIO()
+
+      with stl_handle as sh:
+        display3D(sh, png_file, kw("display3D", False))
+
+      if kw("png") and not basename:
+        rets["png"] = png_file.getvalue()
+
+    if kw("display", False):
+      from rocolib.utils.display import displayTkinter
+      displayTkinter(d)
+
+    return rets
diff --git a/rocolib/api/composables/__init__.py b/rocolib/api/composables/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e49ea2dc6ee809dc1c835ad91907722cf0074393
--- /dev/null
+++ b/rocolib/api/composables/__init__.py
@@ -0,0 +1,2 @@
+from .Composable import Composable
+from .GraphComposable import GraphComposable
diff --git a/rocolib/api/composables/graph/Drawing.py b/rocolib/api/composables/graph/Drawing.py
new file mode 100644
index 0000000000000000000000000000000000000000..44cdabe907632a6a550b84a0a61998c88f43a8fa
--- /dev/null
+++ b/rocolib/api/composables/graph/Drawing.py
@@ -0,0 +1,301 @@
+from math import pi
+import logging
+
+from rocolib.api.composables.graph.DrawingEdge import *
+from rocolib.utils.utils import prefix as prefixString
+
+
+log = logging.getLogger(__name__)
+
+class Drawing:
+  def __init__(self):
+    """
+    Initializes an empty dictionary to contain Edge instances.
+
+    Keys will be Edge labels as strings. Key values will be Edge instances.
+    """
+    self.edges = {}
+
+  def fromGraph(self, g):
+    maxx = 0
+    maxy = 0
+    buffer = 10
+
+    for flist in g.facelists:
+        hyperedges = []
+        for face in flist:
+            for hyperedge in face.edges: 
+                if hyperedge not in hyperedges:
+                    hyperedges.append(hyperedge)
+
+        pts = [p for e in hyperedges for p in e.pts2D]
+        minx = min([x[0] for x in pts])
+        miny = min([x[1] for x in pts])
+        dx = maxx - minx 
+        dy = 0 #dy = maxy + miny 
+
+        maxx = max([x[0] for x in pts]) + dx + buffer
+        maxy = max([x[1] for x in pts]) + dy + buffer
+
+        for e in hyperedges:
+            if e.pts2D is None:
+                log.warning(f"No coordinates for edge {e.name}, ignoring.")
+            else:
+                if len(e.faces) == 1:
+                    edge = Cut()
+                elif len(e.faces) == 2:
+                    angles = list(e.faces.values())
+                    if angles[0][1]:
+                        angle = angles[0][0] - angles[1][0]
+                    else:
+                        angle = angles[1][0] - angles[0][0]
+                    if angle == 0:
+                        edge = Flat()
+                    elif e.edgeType == "BEND":
+                        edge = Flex(angle=angle)
+                    else:
+                        edge = Fold(angle=angle)
+                else:
+                    log.warning(f"Don't know how to handle edge {e.name} with {len(e.faces)} faces, ignoring.")
+                    edge = None
+                self.edges[e.name] = Edge(e.name, e.pts2D[0] + [dx, dy], e.pts2D[1] + [dx, dy], edge)
+                pts.extend(list(e.pts2D))
+        for face in flist:
+            for e in face.get2DDecorations():
+                self.edges[e[0]] = Edge(e[0], e[1] + [dx, dy], e[2] + [dx, dy], EdgeType(e[3], interior=True))
+
+  def toDXF(self, fp, labels=False, mode="dxf"):
+    from dxfwrite import DXFEngine as dxf
+    '''
+    if mode == "silhouette":
+      self.append(Rectangle(12*25.4, 12*25.4, edgetype=Reg()), "outline")
+    '''
+
+    dwg = dxf.drawing()
+    EdgeType.makeLinetypes(dwg, dxf)
+    for e in list(self.edges.items()):
+      e[1].toDrawing(dwg, e[0] if labels else "", mode=mode, engine=dxf)
+    dwg.save_to_fileobj(fp)
+
+    '''
+    if mode == "silhouette":
+      self.edges.pop("outline.e0")
+      self.edges.pop("outline.e1")
+      self.edges.pop("outline.e2")
+      self.edges.pop("outline.e3")
+    '''
+
+  def toSVG(self, fp, labels=False, mode=None):
+    """
+    Writes all Edge instances to a SVG file.
+
+    @type svg:
+    @param svg:
+    @type label: tuple
+    @param label: location of point two in the form (x2,y2).
+    @type mode:
+    @param mode:
+    """
+    import svgwrite
+    
+    minx, miny, maxx, maxy = self.boundingBox()
+    dx = maxx-minx
+    dy = maxy-miny
+
+    svg = svgwrite.Drawing(None, 
+                           size=('%fmm' % dx, '%fmm' % dy), 
+                           viewBox=('%f %f %f %f' % (minx, miny, dx, dy)))
+    for e in list(self.edges.items()):
+      e[1].toDrawing(svg, e[0] if labels else "", mode)
+    svg.write(fp, pretty=True)
+
+  def points(self):
+    """
+    @return: a non-redundant list of all endpoints in tuples
+    """
+    points = []
+    for e in self.edges.values():
+      coords = e.coords()
+      p1 = tuple(coords[0])
+      p2 = tuple(coords[1])
+      points.append(p1)
+      points.append(p2)
+    return list(set(points))
+
+  def edgeCoords(self):
+    """
+    @return: a list of all Edge instance endpoints in Drawing (can include redundant points and edges)
+    """
+    edges = []
+    for e in self.edges.values():
+      edges.append(e.coords())
+    return edges
+
+  def boundingBox(self):
+    pts = [x[0] for x in self.edgeCoords()] + [x[1] for x in self.edgeCoords()]
+
+    minx = min([x[0] for x in pts])
+    miny = min([x[1] for x in pts])
+    maxx = max([x[0] for x in pts])
+    maxy = max([x[1] for x in pts])
+    dx = maxx - minx 
+    dy = maxy + miny 
+
+    return minx, miny, maxx, maxy
+
+  def renameedge(self, fromname, toname):
+    """
+    Renames an Edge instance's Key
+
+    @param fromname: string of the original Edge instance name
+    @param toname: string of the new Edge instance name
+    """
+    self.edges[toname] = self.edges.pop(fromname)
+    self.edges[toname].name = toname
+
+    return self
+
+  def transform(self, scale=1, angle=0, origin=(0,0), relative=None):
+    """
+    Scales, rotates, and translates the Edge instances in Drawing
+
+    @type scale: float
+    @param scale: scaling factor
+    @type angle: float
+    @param angle: angle to rotate in radians
+    @type origin: tuple
+    @param origin: origin
+    @return: Drawing with the new Edge instances.
+    """
+    if relative is not None:
+      minx, miny, maxx, maxy = self.boundingBox()
+      midx = minx + relative[0]*(maxx + minx)
+      midy = miny + relative[1]*(maxy + miny)
+      origin=(origin[0] - midx, origin[1] - midy)
+
+    for e in list(self.edges.values()):
+      e.transform(scale=scale, angle=angle, origin=origin)
+
+    return self
+
+  def mirrorY(self):
+    """
+    Changes the coordinates of Edge instances in Drawing so that they are symmetric about the X axis.
+    @return: Drawing with the new Edge instances.
+    """
+    for e in list(self.edges.values()):
+      e.mirrorY()
+    return self
+
+  def mirrorX(self):
+    """
+    Changes the coordinates of Edge instances in Drawing so that they are symmetric about the Y axis.
+    @return: Drawing with the new Edge instances.
+    """
+    for e in list(self.edges.values()):
+      e.mirrorX()
+    return self
+
+  def flip(self):
+    """
+    Flips the directionality of Edge instances om Drawing around.
+    @return: Drawing with the new Edge instances.
+    """
+    for e in list(self.edges.values()):
+      e.flip()
+    return self
+
+  def append(self, dwg, prefix = '', noGraph=False, **kwargs):
+    for e in list(dwg.edges.items()):
+      self.edges[prefixString(prefix, e[0])] = e[1].copy()
+      self.edges[prefixString(prefix, e[0])].transform(**kwargs)
+    return self
+
+  def duplicate(self, prefix = ''):
+    #Creates a duplicate copy of self.
+    c = Drawing()
+    for e in list(self.edges.items()):
+      c.edges[prefixString(prefix, e[0])] = e[1].copy()
+    return c
+
+  def attach(self, label1, dwg, label2, prefix, edgetype, useOrigName = False):
+    # XXX TODO(mehtank): check to see if attachment edges match?
+    # XXX TOTO(mehtank): make prefix optional?
+
+    if isinstance(label1, (list, tuple)):
+      l1 = label1[0]
+    else:
+      l1 = label1
+      label1 = [label1]
+
+    if isinstance(label2, (list, tuple)):
+      l2 = label2[0]
+    else:
+      l2 = label2
+      label2 = [label2]
+
+    if isinstance(edgetype, (list, tuple)):
+      e12 = edgetype[0]
+    else:
+      e12 = edgetype
+      edgetype = [edgetype] * len(label1)
+
+    #create a copy of the new drawing to be attached
+    d = dwg.duplicate()
+
+    #move the edge of the new drawing to be attached to the origin
+    d.transform(origin=(-d.edges[l2].x2, -d.edges[l2].y2))
+
+    #don't rescale
+    scale = 1
+
+    #find angle to rotate new drawing to align with old drawing edge
+    phi   = self.edges[l1].angle()
+    angle = phi - d.edges[l2].angle() + pi
+
+    #align edges offset by a separation of distance between the start points
+    d.transform(scale=scale, angle=angle, origin=(self.edges[l1].coords()[0][0], self.edges[l1].coords()[0][1]))
+
+    for e in list(d.edges.items()):
+      try:
+        i = label2.index(e[0])
+        e[1].edgetype = edgetype[i]
+        if useOrigName:
+          e[1].name = label1[label2.index(e[0])]
+          self.edges[label1[label2.index(e[0])]] = e[1]
+        else:
+          self.edges.pop(label1[label2.index(e[0])])
+          e[1].name = prefix + '.' + e[0]
+          self.edges[prefix + '.' + e[0]] = e[1]
+      except ValueError:
+        e[1].name = prefix + '.' + e[0]
+        self.edges[prefix + '.' + e[0]] = e[1]
+
+  def times(self, n, fromedge, toedge, label, mode):
+    d = Drawing()
+    d.append(self, label+'0')
+    for i in range(1, n):
+      d.attach(label+repr(i-1)+'.'+toedge, self, fromedge, label+repr(i), mode)
+    return d
+
+class Face(Drawing):
+  def __init__(self, pts, edgetype = None, origin = True):
+    Drawing.__init__(self)
+    if origin:
+      pts = list(pts) + [(0,0)]
+    else:
+      pts = list(pts)
+
+    lastpt = pts[-1]
+    edgenum = 0
+    edgenames = []
+    for pt in pts:
+      name = 'e%d' % edgenum
+      self.edges[name] = Edge(name, lastpt, pt, edgetype)
+      edgenames.append(name)
+      lastpt = pt
+      edgenum += 1
+
+class Rectangle(Face):
+  def __init__(self, l, w, edgetype = None, origin = True):
+    Face.__init__(self, ((l, 0), (l, w), (0, w), (0,0)), edgetype, origin)
diff --git a/rocolib/api/composables/graph/DrawingEdge.py b/rocolib/api/composables/graph/DrawingEdge.py
new file mode 100644
index 0000000000000000000000000000000000000000..25d041316de639decd79bda8825845323698d1e2
--- /dev/null
+++ b/rocolib/api/composables/graph/DrawingEdge.py
@@ -0,0 +1,355 @@
+import rocolib.utils.numsym as np
+
+
+class EdgeType:
+  """
+  Values for the different modes of Edge instances
+  """
+  # ( Name, SVG color, DXF color, Layer name )
+  TYPENAME, TYPEFOLD, TYPEDISP, TYPESVG, TYPEDXF, TYPELAYER, TYPEDRAW = list(range(7))
+
+  TYPEDATA = ( 
+    ( "REGISTRATION", False, "green",  "#00ff00", 6, "reg", True),
+    ( "BOUNDARY_CUT", False, "black",  "#000000", 5, "cut", True),
+    ( "INTERIOR_CUT", False, "gray",   "#888888", 5, "cut", True),
+    ( "FOLD",         True,  ("blue", "red"),    ("#0000ff", "#ff0000"), (1, 3), "xxx", True),
+    ( "FLEX",         True,  ("yellow", "cyan"), ("#00ffff", "#ffff00"), (1, 3), "xxx", False),
+    ( "FLAT",         False, "white",  "#ffffff", 3, "nan", False),
+  )
+  
+  ( REGISTRATION,
+    BOUNDARY_CUT,
+    INTERIOR_CUT,
+    FOLD,
+    FLEX,
+    FLAT,
+  ) = list(range(len(TYPEDATA)))
+
+  def __init__(self, edgetype, angle=0, interior=False):
+    self.edgetype = edgetype
+    if interior and edgetype == self.BOUNDARY_CUT:
+        self.edgetype = self.INTERIOR_CUT
+    if interior and edgetype == self.FOLD:
+        self.edgetype = self.FLEX
+    self.typedata = EdgeType.TYPEDATA[self.edgetype]
+    self.angle = (angle + 180) % 360 - 180
+    if angle == 180: 
+        self.angle = 180
+
+  def __repr__(self):
+    ret = self.typedata[self.TYPENAME]
+    if self.angle:
+      ret += " (%d)" % self.angle
+    return ret
+
+  @classmethod
+  def makeLinetypes(cls, drawing, dxf):
+      drawing.add_linetype("DOTTED", pattern=dxf.linepattern([1, 0, -1]))
+
+  def dispColor(self, showFlats = True):
+    if self.edgetype == self.FLAT and not showFlats:
+      return None
+
+    if self.typedata[self.TYPEFOLD]:
+      return self.typedata[self.TYPEDISP][self.angle < 0]
+    else:
+      return self.typedata[self.TYPEDISP]
+
+  def drawArgs(self, name, mode):
+    if not(self.typedata[self.TYPEDRAW]): 
+      return
+
+    coloridx = 0
+    if mode in ("silhouette", "print", "animate") and self.angle < 0:
+      coloridx = 1
+
+    # DXF output
+    if mode in ("dxf", "silhouette", "autofold"):
+      if self.typedata[self.TYPEFOLD]:
+        ret = {"linetype": "DOTTED",
+               "color": self.typedata[self.TYPEDXF][coloridx]}
+      else:
+        ret = {"color": self.typedata[self.TYPEDXF]}
+
+      if mode == "autofold":
+        ret["layer"] = self.typedata[self.TYPELAYER]
+        if self.typedata[self.TYPEFOLD]:
+          ret["layer"] = repr(self.angle)
+
+
+    # SVG output
+    else:
+      ret = {"id" : name}
+
+      if self.typedata[self.TYPEFOLD]:
+        ret = {"stroke": self.typedata[self.TYPESVG][coloridx]}
+        if mode == "print":
+          ret["stroke-dasharray"] = "2 6"
+          ret["stroke-dashoffset"] = "5"
+        elif mode == "animate":
+          angle = self.angle
+          ret["opacity"] = abs(angle) / 180.
+      else:
+        ret = {"stroke": self.typedata[self.TYPESVG]}
+
+    return ret
+
+class Flat(EdgeType):
+  def __init__(self):
+    EdgeType.__init__(self, EdgeType.FLAT)
+class Reg(EdgeType):
+  def __init__(self):
+    EdgeType.__init__(self, EdgeType.REGISTRATION)
+class Cut(EdgeType):
+  def __init__(self):
+    EdgeType.__init__(self, EdgeType.BOUNDARY_CUT)
+def Fold(angle=0):
+  if isinstance(angle, (list, tuple)):
+    return [EdgeType(EdgeType.FOLD, angle=x) for x in angle]
+  else:
+    return EdgeType(EdgeType.FOLD, angle=angle)
+def Flex(angle=0):
+  if isinstance(angle, (list, tuple)):
+    return [EdgeType(EdgeType.FLEX, angle=x) for x in angle]
+  else:
+    return EdgeType(EdgeType.FLEX, angle=angle)
+
+def diag(dx, dy):
+  """
+  Returns the diagonal distance between two points.
+
+  :param dx: the change in x distance between the two points
+  :type dx: real number
+  :param dy: the change in y distance between the two points
+  :type dy: real number
+  :returns: the diagonal distance between two points
+  :rtype: numpy.float64
+  """
+  return np.sqrt(dx*dx + dy*dy)
+
+class Edge:
+  """
+  A class representing an Edge.
+  """
+
+  def __init__(self, name, pt1, pt2, edgetype):
+    """
+    Initializes an Edge object with pt1 and pt2 in the form ((x1,y1),(x2,y2))
+
+    The Edge can have 5 different types: CUT, FLAT, BEND, FOLD, TAB
+
+    :param pt1: location of point one in the form (x1, y1)
+    :type pt1: tuple
+    :param pt2: location of point one in the form (x2, y2)
+    :type pt2: tuple
+    :param mode: 5 different types of Edges: CUT, FLAT, BEND, FOLD, TAB
+    :type mode: string
+    """
+
+    self.name = name
+    self.x1 = pt1[0]
+    self.y1 = pt1[1]
+    self.x2 = pt2[0]
+    self.y2 = pt2[1]
+    if edgetype is None:
+      edgetype = Cut()
+    self.edgetype = edgetype
+
+  def coords(self):
+    """
+    :returns: a list of the coordinates of the Edge instance endpoints
+    :rtype: list of [[x1,y1],[x2,y2]] rounded to the nearest 1e-6
+    """
+
+    coords  = [[round(self.x1, 6),round(self.y1,6)],[round(self.x2,6),round(self.y2,6)]]
+    for i in coords:
+      if i[0] == -0.0:
+        i[0] = 0.0
+      if i[1] == -0.0:
+        i[1] = 0.0
+    return coords
+
+  def length(self):
+    """
+    Uses the diag() function
+
+    :returns: the length of the edge
+    :rtype: np.float64
+    """
+    dx = self.x2 - self.x1
+    dy = self.y2 - self.y1
+    return diag(dx, dy)
+
+  def angle(self, deg=False):
+    """
+    :param deg: sets the angle return type to be deg or rad
+    :type deg: boolean
+
+    :returns: angle of the Edge instance wrt the positive x axis
+    :rtype: numpy.float64
+    """
+    dx = self.x2 - self.x1
+    dy = self.y2 - self.y1
+    ang = np.arctan2(dy, dx)
+    if deg:
+      return np.rad2deg(ang)
+    else:
+      return ang
+
+  def elongate(self, lengths, otherway = False):
+    """
+    Returns a list of Edge instances that extend out from the endpoint of another Edge instance.
+    Mode of all smaller edges is the same as the original Edge instance.
+
+    :param lengths: list of lengths to split the Edge instance into
+    :type lengths: list
+    :param otherway: boolean specifying where to start from (pt2 if otherway == False, pt1 if otherway == True)
+    :type otherway: boolean
+
+    :returns: a list of Edge instances that extend out from the endpoint of another Edge instance
+    :rtype: a list of Edge instances
+    """
+
+    edges = []
+    if otherway:
+      lastpt = (self.x1, self.y1)
+      for length in lengths:
+        e = Edge((0, 0),(-length,0), self.edgetype)
+        e.transform(angle=self.angle(), origin=lastpt)
+        lastpt = (e.x2, e.y2)
+      edges.append(e)
+    else:
+      lastpt = (self.x2, self.y2)
+      for length in lengths:
+        e = Edge((0,0), (length, 0), self.edgetype)
+        e.transform(angle=self.angle(), origin=lastpt)
+        lastpt = (e.x2, e.y2)
+      edges.append(e)
+
+    return edges
+
+
+  def transform(self, scale=1, angle=0, origin=(0,0)):
+    """
+    Scales, rotates, and translates an Edge instance.
+
+    :param scale: scaling factor
+    :type scale: float
+    :param angle: angle to rotate in radians
+    :type angle: float
+    :param origin: origin
+    :type origin: tuple
+    """
+
+    r = np.array([[np.cos(angle), -np.sin(angle)],
+                  [np.sin(angle),  np.cos(angle)]]) * scale
+
+    o = np.array(origin)
+
+    pt1 = np.dot(r, np.array((self.x1, self.y1))) + o
+    pt2 = np.dot(r, np.array((self.x2, self.y2))) + o
+
+    self.x1 = pt1[0]
+    self.y1 = pt1[1]
+    self.x2 = pt2[0]
+    self.y2 = pt2[1]
+
+  def invert(self):
+    """
+    Swaps mountain and valley folds
+    """
+    self.edgetype.invert()
+
+  def mirrorX(self):
+    """
+    Changes the coordinates of an Edge instance so that it is symmetric about the Y axis.
+    """
+    self.x1 = -self.x1
+    self.x2 = -self.x2
+    self.flip()
+
+  def mirrorY(self):
+    """
+    Changes the coordinates of an Edge instance so that it is symmetric about the X axis.
+    """
+    self.y1 = -self.y1
+    self.y2 = -self.y2
+    self.flip()
+
+  def flip(self):
+    """
+    Flips the directionality of an Edge instance around
+    """
+    x = self.x2
+    y = self.y2
+    self.x2 = self.x1
+    self.y2 = self.y1
+    self.x1 = x
+    self.y1 = y
+
+  def copy(self):
+    return Edge(self.name, (self.x1, self.y1), (self.x2, self.y2), self.edgetype)
+
+  def midpt(self):
+    """
+    :returns: a tuple of the edge midpoint
+    :rtype: tuple
+    """
+    pt1 = self.coords()[0]
+    pt2 = self.coords()[1]
+    midpt = ((pt2[0]+pt1[0])/2, (pt2[1]+pt1[1])/2)
+    return midpt
+
+  def dispColor(self, showFlats = True):
+    return self.edgetype.dispColor(showFlats)
+
+  def toDrawing(self, drawing, label="", mode=None, engine=None):
+    """
+    Draws an Edge instance to a CAD file.
+
+    :type drawing:
+    :param drawing:
+    :type label: tuple
+    :param label: location of point two in the form (x2,y2).
+    :type mode:
+    :param mode:
+    """
+
+    if engine is None:
+      engine = drawing
+
+    kwargs = self.edgetype.drawArgs(self.name, mode)
+    if kwargs:
+
+      dpi = None
+
+      if mode in ( 'Corel'):
+        dpi = 96 # scale from mm to 96dpi for CorelDraw
+      elif mode == 'Inkscape':
+        dpi = 90 # scale from mm to 90dpi for Inkscape
+      elif mode == 'autofold':
+        if str(self.edgetype.angle) not in drawing.layers:
+          drawing.add_layer(str(self.edgetype.angle))
+
+      if dpi: self.transform(scale=(dpi/25.4))
+      drawing.add(engine.line((float(self.x1), float(self.y1)), (float(self.x2), float(self.y2)), **kwargs))
+      if dpi: self.transform(scale=(25.4/dpi)) # scale back to mm
+
+    if label:
+      r = [int(self.angle(deg=True))]*len(label)
+      t = engine.text(label, insert=((self.x1+self.x2)/2, (self.y1+self.y2)/2))# , rotate=r)
+      # t.rotate=r
+      drawing.add(t)
+
+if __name__ == "__main__":
+  import svgwrite
+  e = Edge("e1", (0,0), (1,1), Flex())
+  svg = svgwrite.Drawing("testedge.svg")
+  e.toDrawing(svg, mode="Inkscape")
+  svg.save()
+
+  from dxfwrite import DXFEngine as dxf
+  svg = dxf.drawing("testedge.dxf")
+  e.toDrawing(svg, mode="dxf", engine=dxf)
+  svg.save()
+
diff --git a/rocolib/api/composables/graph/Face.py b/rocolib/api/composables/graph/Face.py
new file mode 100644
index 0000000000000000000000000000000000000000..a01059ba9c2605c92883f1a662a2ff73dfcfbf81
--- /dev/null
+++ b/rocolib/api/composables/graph/Face.py
@@ -0,0 +1,423 @@
+import logging
+
+from rocolib.api.composables.graph.HyperEdge import *
+from rocolib.utils.transforms import *
+from rocolib.utils.utils import prefix as prefixString
+import rocolib.utils.numsym as np
+
+
+log = logging.getLogger(__name__)
+
+class Face(object):
+  allNames = []
+
+  def __init__(self, name, pts, edgeNames=True, edgeAngles=None, edgeFlips=None, allEdges=None, decorations=None, recenter=True):
+    if name:
+      self.name = name
+    else:
+      self.name = "" # "face%03d" % len(Face.allNames)
+    Face.allNames.append(self.name)
+
+    self.recenter(list(pts), recenter=recenter)
+
+    self.edges = [None] * len(pts)
+    if edgeNames is True:
+      edgeNames = ["e%d" % i for i in range(len(pts))]
+    self.renameEdges(edgeNames, edgeAngles, edgeFlips, allEdges)
+
+    if decorations:
+      self.decorations = decorations
+    else:
+      self.decorations = []
+
+    self.transform2D = None
+    self.transform3D = None
+    self.inverted = False
+
+    self.joinedFaces = []
+
+  def recenter(self, pts, recenter=True):
+    self.pts2d = [(p[0], p[1]) for p in pts]
+
+    # Put centroid of polygon at origin
+    xs = [p[0] for p in pts] + [pts[0][0]]
+    ys = [p[1] for p in pts] + [pts[0][1]]
+
+    a, cx, cy = 0, 0, 0
+    for i in range(len(pts)):
+      a += (xs[i] * ys[i+1] - xs[i+1] * ys[i]) / 2
+      cx += (xs[i] + xs[i+1]) * (xs[i] * ys[i+1] - xs[i+1] * ys[i]) / 6
+      cy += (ys[i] + ys[i+1]) * (xs[i] * ys[i+1] - xs[i+1] * ys[i]) / 6
+
+    self.area = a
+    # XXX Hack -- what should we do if the area is 0?
+    if a == 0:
+      self.pts2d = [(p[0], p[1]) for p in pts]
+      self.com2d = (0, 0)
+    else: 
+      if recenter:
+        self.pts2d = [(p[0] - cx/a, p[1] - cy/a) for p in pts]
+        self.com2d = (0, 0)
+      else:
+        self.pts2d = [(p[0], p[1]) for p in pts]
+        self.com2d = (cx/a, cy/a)
+
+
+  def pts4d(self):
+    return np.transpose(np.array([list(x) + [0,1] for x in self.pts2d]))
+
+  def com4d(self):
+    return np.transpose(np.array(list(self.com2d) + [0,1]))
+
+  def rename(self, name):
+    self.name = name
+
+  def prefix(self, prefix):
+    self.name = prefixString(prefix, self.name)
+    self.prefixEdges(prefix)
+
+  def prefixEdges(self, prefix):
+    for e in self.edges:
+      e.rename(prefixString(prefix, e.name))
+
+  def renameEdges(self, edgeNames=None, edgeAngles=None, edgeFlips=None, allEdges=None):
+    if edgeNames:
+      if edgeAngles is None:
+        edgeAngles = [0] * len(edgeNames)
+      if edgeFlips is None:
+        edgeFlips = [False] * len(edgeNames)
+      for (index, name) in enumerate(edgeNames):
+        self.setEdge(index, name, edgeAngles[index], edgeFlips[index], allEdges)
+    return self
+
+  def setEdge(self, index, name=None, angle=None, flip=False, allEdges=None):
+    if name is None:
+      return self
+    try:
+      if self.edges[index].name == name:
+        if angle is not None:
+          self.edges[index].setAngle(angle, flip)
+        return self
+    except:
+      pass
+
+    self.disconnect(index)
+
+    e = HyperEdge.edge(allEdges, name, length=self.edgeLength(index), face=self, angle=angle, flip=flip)
+    self.edges[index] = e
+
+    return self
+
+  def replaceEdge(self, oldEdge, newEdge, angle, flip):
+    for (i, e) in enumerate(self.edges):
+      if e is oldEdge:
+        self.disconnect(i)
+        self.edges[i] = newEdge
+        newEdge.join(self.edgeLength(i), self, angle=angle, flip=flip)
+    return self
+
+  def edgeIndex(self, name):
+    for (i, e) in enumerate(self.edges):
+      if name == e.name:
+        return i
+
+  def edgeCoords(self, index):
+    return (self.pts2d[index-1], self.pts2d[index])
+
+  def edgeLength(self, edgeIndex):
+    coords = self.edgeCoords(edgeIndex)
+    pt1 = np.array(coords[0])
+    pt2 = np.array(coords[1])
+
+    d = pt2 - pt1
+    return np.norm(d)
+
+  def rotate(self, n=1):
+    for i in range(n):
+      self.edges.append(self.edges.pop(0))
+      self.pts2d.append(self.pts2d.pop(0))
+
+    return self
+
+  def flip(self):
+    newEdges = []
+    newPts = []
+    while self.edges:
+      newEdges.append(self.edges.pop())
+      newEdges[-1].flipConnection(self)
+      newPts.append(self.pts2d.pop())
+    newEdges.insert(0, newEdges.pop())
+    self.edges = newEdges
+    self.pts2d = newPts
+    return self
+
+  def transform(self, scale=1, angle=0, origin=(0,0)):
+    r = np.array([[np.cos(angle), -np.sin(angle)],
+                  [np.sin(angle),  np.cos(angle)]]) * scale
+    o = np.array([origin] * len(self.pts2d))
+
+    pts = np.transpose(np.dot(r, np.transpose(np.array(self.pts2d)))) + o
+    self.pts2d = [tuple(x) for x in np.rows(pts)]
+    for (i, d) in enumerate(self.decorations):
+      o = np.array([origin] * len(d[0]))
+      pts = np.transpose(np.dot(r, np.transpose(np.array(d[0])))) + o
+      self.decorations[i] = ([tuple(x) for x in np.rows(pts)], d[1])
+
+  def disconnectFrom(self, edgename):
+    for (i, e) in enumerate(self.edges):
+      if edgename == e.name:
+        return self.disconnect(i)
+    return self
+
+  def disconnectAll(self):
+    for i in range(len(self.edges)):
+      self.disconnect(i)
+    return self
+
+  def disconnect(self, index):
+    e = self.edges[index]
+
+    if e is None:
+      return self
+
+    self.edges[index] = None
+    e.remove(self)
+    return self
+
+  def allNeighbors(self):
+    n = []
+    for es in self.neighbors():
+      n.extend(es)
+    return n
+
+  def neighbors(self):
+    n = []
+    for e in self.edges:
+      if e is None:
+        n.append([])
+      else:
+        n.append([f.name for f in e.faces if f.name != self.name])
+    return n
+
+  def copy(self, name):
+    return Face(name, self.pts2d, decorations=self.decorations, recenter=False)
+    
+  def matches(self, other):
+    if len(self.pts2d) != len(other.pts2d):
+      return False
+    #XXX TODO: verify congruence
+    bothpts = list(zip(self.pts2d, other.pts2d))
+    return True
+
+  def addDecoration(self, pts):
+    self.decorations.append(pts)
+
+  def addFace(self, face, transform):
+    self.joinedFaces.append((face, transform))
+
+  def preTransform(self, edge):
+    index = self.edges.index(edge)
+    return np.dot(RotateOntoX(*self.edgeCoords(index)), MoveToOrigin(self.pts2d[index]))
+
+  def placeagain(self):
+    coords2D = self.get2DCoords()
+    coords3D = self.get3DCoords()
+    for (i, e) in enumerate(self.edges):
+      da = e.faces[self]
+      if da[1]:
+        e.place((coords2D[:,i-1], coords2D[:,i]), (coords3D[:,i-1], coords3D[:,i]))
+      else:
+        e.place((coords2D[:,i], coords2D[:,i-1]), (coords3D[:,i], coords3D[:,i-1]))
+
+  def place(self, edgeFrom, transform2D, transform3D, facelists, flind=None):
+    if self.transform2D is not None or self.transform3D is not None:
+      # TODO : verify that it connects appropriately along alternate path
+      return
+
+    if edgeFrom is not None:
+      r = self.preTransform(edgeFrom)
+    else:
+      r = np.eye(4)
+
+    if transform2D is None:
+        transform2D = np.eye(4)
+        self.transform2D = r
+        flind = len(facelists)
+        facelists.append([self])
+    else:
+        self.transform2D = np.dot(transform2D, r)
+        facelists[flind].append(self)
+
+    self.transform3D = np.dot(transform3D, r)
+
+    pts2d = np.dot(r, self.pts4d())[0:2,:]
+
+    coords2D = self.get2DCoords()
+    coords3D = self.get3DCoords()
+
+    # Follow all non-joints before joints
+    for (i, e) in sorted(enumerate(self.edges), key=lambda x : x[1].isJoint()):
+      # XXX hack: don't follow small edges
+      if e is None or e.isTab():
+        continue
+      
+      el = self.edgeLength(i)
+      try:
+          if el <= 0.01:
+            log.info(f'Skipping traversal of short edge, length = {el}')
+            continue
+      except TypeError:
+        log.info(f'Sympyicized variable detected - ignoring edge length check on edge {e.name}')
+      
+      da = e.faces[self]
+
+      if len(e.faces) <= 1:
+        # No other faces to be found, move on to next edge.
+        continue
+
+      pt1 = pts2d[:,i-1]
+      pt2 = pts2d[:,i]
+
+      # TODO : Only skip self and the face that you came from to verify multi-connected edges
+      # XXX : Assumes both faces have opposite edge orientation
+      #       Only works for non-hyper edges -- need to store edge orientation info for a +/- da
+      for (facename, value) in list(e.faces.items()):
+        if value[1] ^ da[1]:
+          # opposite orientation
+          pta, ptb = pt1, pt2
+        else:
+          # same orientation
+          pta, ptb = pt2, pt1
+
+        x = RotateXTo(ptb, pta)
+
+        if e.isJoint():
+            t2d = None
+        else:
+            r2d = np.eye(4)
+            r2d = np.dot(x, r2d)
+            r2d = np.dot(MoveOriginTo(pta), r2d)
+            t2d = np.dot(transform2D, r2d)
+
+        #Combine the following two lines in an appropriately symbolic way
+        #  r3d = RotateX(np.deg2rad(value[0]+da[0]))
+        #  r3d = np.dot(x, r3d)
+        r3d = np.dotrot(x, RotateX, value[0]+da[0])
+        r3d = np.dot(MoveOriginTo(pta), r3d)
+        t3d = np.dot(transform3D, r3d)
+
+        facename.place(e, t2d, t3d, facelists, flind)
+        # end for faces.iteritems
+
+    for e in self.edges:
+      if e.isJoint():
+        index = self.edgeIndex(e.name)
+            # print "Jointing ", e.name, "on face", self.name, index
+        newPts, newEdges = e.joint.go(self, e)
+        self.pts2d[index:index] = newPts                                         
+        self.edges[index:index+1] = newEdges                                     
+        for newEdge in newEdges:                                                 
+          newEdge.join(newEdge.length, self)                                   
+        e.remove(self)
+
+    # now place attached faces
+    for (f, t) in self.joinedFaces:
+        if self.inverted:
+            t3d = np.dot(MirrorZ(), t)
+        else:
+            t3d = t
+        t3d = np.dot(self.transform3D, t3d)
+        f.place(None, None, t3d, facelists)
+
+    self.placeagain()
+
+  def getTriangleDict(self):
+    vertices = self.pts2d
+    segments = [(i, (i+1) % len(vertices)) for i in range(len(vertices))]
+
+    holes = []
+
+    for d in ( x[0] for x in self.decorations if x[1] == "hole" ):
+      lv = len(vertices)
+      ld = len(d)
+      vertices.extend( d )
+      segments.extend( [(lv + ((i+1) % ld), lv+i) for i in range(ld)] )
+      holes.append( tuple(sum([np.array(x) for x in d])/len(d) ))
+
+    if holes:
+      return dict(vertices=(vertices), segments=(segments), holes=(holes))
+    else:
+      return dict(vertices=(vertices), segments=(segments))
+
+  def get2DCoords(self):
+    if self.transform2D is not None:
+      return np.dot(self.transform2D, self.pts4d())[0:2,:]
+
+  def get2DCOM(self):
+    if self.transform2D is not None:
+      return np.dot(self.transform2D, self.com4d())[0:2,:]
+
+  def get2DDecorations(self):
+    if self.transform2D is not None:
+      edges = []
+      for i, e in enumerate(self.decorations):
+        if e[1] == "hole":
+          for j in range(len(e[0])):
+            name = self.name + ".d%d.e%d" % (i,j)
+            pt1 = np.dot(self.transform2D, np.array(list(e[0][j-1]) + [0,1]))[0:2]
+            pt2 = np.dot(self.transform2D, np.array(list(e[0][j]) + [0,1]))[0:2]
+            # XXX use EdgeType appropriately
+            edges.append([name, pt1, pt2, 1])
+        else:
+          name = self.name + ".d%d" % i
+          pt1 = np.dot(self.transform2D, np.array(list(e[0][0]) + [0,1]))[0:2]
+          pt2 = np.dot(self.transform2D, np.array(list(e[0][1]) + [0,1]))[0:2]
+          edges.append([name, pt1, pt2, e[1]])
+      return edges
+    return []
+
+  def get3DCoords(self):
+    if self.transform3D is not None:
+      return np.dot(self.transform3D, self.pts4d())[0:3,:]
+
+  def get3DCOM(self):
+    if self.transform3D is not None:
+      return np.dot(self.transform3D, self.com4d())[0:3,:]
+
+  # Nov 2020: Removed for conversion for py2 to py3 
+  #def __eq__(self, other):
+    #return self.name == other.name
+
+class RegularNGon(Face):
+  def __init__(self, name, n, length, edgeNames=True, allEdges=None):
+    pts = []
+    lastpt = (0, 0)
+    dt = (2 * np.pi() / n)
+    for i in range(n):
+      lastpt = (lastpt[0] + length*np.cos(i * dt), lastpt[1] + length*np.sin(i * dt))
+      pts.append(lastpt)
+
+    Face.__init__(self, name, pts, edgeNames=edgeNames, allEdges=allEdges)
+
+class RegularNGon2(Face):
+  def r2l(r, n):
+    return r*2*np.sin(np.pi()/n)
+
+  def __init__(self, name, n, radius, edgeNames=True, allEdges=None):
+    pts = []
+    dt = (2 * np.pi() / n)
+    for i in range(n):
+      pts.append((radius*np.cos(i * dt), radius*np.sin(i * dt)))
+    Face.__init__(self, name, pts, edgeNames=edgeNames, allEdges=allEdges)
+
+class Square(RegularNGon):
+  def __init__(self, name, length, edgeNames=True, allEdges=None):
+    RegularNGon.__init__(self, name, 4, length, edgeNames=edgeNames, allEdges=allEdges)
+
+class Rectangle(Face):
+  def __init__(self, name, l, w, edgeNames=True, allEdges=None, recenter=True):
+    Face.__init__(self, name, ((l, 0), (l, w), (0, w), (0,0)), edgeNames=edgeNames, allEdges=allEdges, recenter=recenter)
+
+class RightTriangle(Face):
+  def __init__(self, name, l, w, edgeNames=True, allEdges=None):
+    Face.__init__(self, name, ((l, 0), (0, w), (0,0)), edgeNames=edgeNames, allEdges=allEdges)
diff --git a/rocolib/api/composables/graph/Graph.py b/rocolib/api/composables/graph/Graph.py
new file mode 100644
index 0000000000000000000000000000000000000000..46dafa9dee591a9f348754930893ae94b37e2b53
--- /dev/null
+++ b/rocolib/api/composables/graph/Graph.py
@@ -0,0 +1,344 @@
+import logging
+
+from rocolib.api.composables.graph.HyperEdge import HyperEdge
+from rocolib.utils.utils import prefix as prefixString
+import rocolib.utils.numsym as np
+
+
+log = logging.getLogger(__name__)
+
+def inflate(face, thickness=.1, edges=False):
+  dt = np.array([[0],[0],[thickness/2.],[0]])
+  nf = face-dt
+  pf = face+dt
+
+  faces = []
+
+  if edges:
+    faces.append(np.transpose(np.array((pf[:,0], nf[:,0], pf[:,1]))))
+    faces.append(np.transpose(np.array((nf[:,0], nf[:,1], pf[:,1]))))
+  else:
+    faces.append(pf)          # top face
+    faces.append(nf[:,::-1])  # bottom face
+
+  return faces
+
+def STLWrite(faces, fp, **kwargs):
+  scale = .001 # roco units : mm ; STL units m
+
+  from .stlwriter import ASCII_STL_Writer as STL_Writer
+  import triangle
+
+  shells = []
+  triangles = []
+  for i, f in enumerate(faces):
+    r = f[0]
+    A = f[1]
+
+    facets = []
+    B = triangle.triangulate(A, opts='p')
+    if not 'triangles' in B:
+      log.warning("No triangles in " + f[2])
+      continue
+
+    thickness = "thickness" in kwargs and kwargs["thickness"]
+    if thickness:
+      for t in [np.transpose(np.array([list(B['vertices'][x]) + [0,1] for x in (face[0], face[1], face[2])])) for face in B['triangles']]:
+        facets.extend([np.dot(r, x) * scale for x in inflate(t, thickness=thickness)])
+      for t in [np.transpose(np.array([list(A['vertices'][x]) + [0,1] for x in (edge[0], edge[1])])) for edge in A['segments']]:
+        facets.extend([np.dot(r, x) * scale for x in inflate(t, thickness=thickness, edges=True)])
+    else:
+      for t in [np.transpose(np.array([list(B['vertices'][x]) + [0,1] for x in (face[0], face[1], face[2])])) for face in B['triangles']]:
+        facets.append(np.dot(r, t) * scale)
+
+    triangles.extend(facets)
+    '''
+    # Output each face to its own STL file 
+    if filename:
+        with open(filename.replace(".stl", "_%02d.stl" % i), 'wb') as fp:
+          writer = STL_Writer(fp)
+          writer.add_faces(facets)
+          writer.close()
+    '''
+
+  faces = triangles
+  writer = STL_Writer(fp)
+  writer.add_faces(faces)
+  writer.close()
+
+class Graph():
+  def __init__(self):
+    self.faces = []
+    self.facelists = []
+    self.edges = []
+
+  def addFace(self, f, prefix=None, root=False, faceEdges=None, faceAngles=None, faceFlips=None):
+    if prefix:
+      f.prefix(prefix)
+    if f in self.faces:
+      raise ValueError("Face %s already in graph" % f.name)
+    if root:
+        self.faces.insert(0, f)
+    else:
+        self.faces.append(f)
+
+    if faceEdges is not None:
+      f.renameEdges(faceEdges, faceAngles, faceFlips, self.edges)
+      if prefix:
+        f.prefixEdges(prefix)
+
+    self.rebuildEdges()
+    return self
+
+  def attachFace(self, fromFace, toFace, prefix=None, transform=None):
+    self.addFace(toFace, prefix)
+    self.mergeFace(fromFace, toFace.name, transform)
+
+  def mergeFace(self, fromFaceName, toFaceName, transform=None):
+    fromFace = self.getFace(fromFaceName)
+    toFace = self.getFace(toFaceName)
+    if transform is None:
+        transform = np.eye(4)
+    fromFace.addFace(toFace, transform)
+    toFace.addFace(fromFace, np.inv(transform))
+
+  def attachEdge(self, fromEdge, newFace, newEdge, prefix=None, root=False, angle=0, edgeType=None, joints=None):
+    # XXX should set angle from a face, not absolute angle of the face
+    self.addFace(newFace, prefix, root)
+
+    if fromEdge is not None:
+      newEdge = prefixString(prefix, newEdge)
+      self.mergeEdge(fromEdge, newEdge, angle=angle, edgeType=edgeType, joints=joints)
+
+  def delFace(self, facename):
+    for (i, f) in enumerate(self.faces):
+      if f.name == facename:
+        f.disconnectAll()
+        self.faces.pop(i)
+        self.rebuildEdges()
+        return self
+
+    return self
+
+  def getFace(self, name):
+    for f in self.faces:
+      if f.name == name:
+        return f
+    return None
+
+  def getEdge(self, name):
+    for e in self.edges:
+      if e.name == name:
+        return e
+    return None
+
+  def prefix(self, prefix):
+    for e in self.edges:
+      e.rename(prefixString(prefix, e.name))
+    for f in self.faces:
+      f.rename(prefixString(prefix, f.name))
+
+  def renameEdge(self, fromname, toname):
+    e = self.getEdge(fromname)
+    if e:
+      e.rename(toname)
+
+  def rebuildEdges(self):
+    self.edges = []
+    for f in self.faces:
+      for e in f.edges:
+        if e not in self.edges:
+          self.edges.append(e)
+
+  def invertEdges(self):
+    # swap mountain and valley folds
+    for e in self.edges:
+      for f in e.faces:
+        e.faces[f] = (-e.faces[f][0], e.faces[f][1])
+    for f in self.faces:
+      f.inverted = not f.inverted
+
+  def addTab(self, edge1, edge2, angle=0, width=10):
+    self.mergeEdge(edge1, edge2, angle=angle, tabWidth=width)
+
+  def mergeEdge(self, edge1, edge2, angle=0, tabWidth=None, edgeType=None, joints=None, swap=False):
+    e1 = self.getEdge(edge1)
+    e2 = self.getEdge(edge2)
+    if e1 is None or e2 is None:
+      logstr  = f"Edge not found trying to merge ({edge1}, {edge2}) in edges:\n  "
+      logstr += "\n  ".join([e.name for e in self.edges])
+      log.error(logstr)
+      raise AttributeError("Edge not found")
+    if swap:
+      e1, e2 = e2, e1
+
+    if len(e2.faces) > 1:
+      log.warning("Adding more than two faces to an edge, currently not well supported.")
+      e2.mergeWith(e1, angle=angle, flip=False, tabWidth=tabWidth)
+    else:
+      e2.mergeWith(e1, angle=angle, flip=True, tabWidth=tabWidth)
+    self.edges.remove(e1)
+    
+    e2.setType(edgeType)
+    if joints:
+        for joint in joints.joints:
+            e2.addJoint(joint)
+
+    return self
+
+  def splitEdge(self, edge):
+    old_edge = edge
+    old_edge_name = edge.name
+    new_edges_and_faces = []
+
+    for i, face in enumerate(list(old_edge.faces)):
+        length = old_edge.length
+        angle = old_edge.faces[face][0]
+        flip = old_edge.faces[face][1]
+
+        new_edge_name = old_edge_name + '.se' + str(i)
+        new_edge = HyperEdge(new_edge_name, length)
+        face.replaceEdge(old_edge, new_edge, angle, flip=False )
+        new_edges_and_faces.append((new_edge_name, face, length, angle, flip))
+
+    self.rebuildEdges()
+    return new_edges_and_faces
+
+  def jointify(self, **kwargs):
+    for e in self.edges:
+        if e.isNotFlat() and "joint" in kwargs:
+            e.setType("JOINT")
+            e.addJoint(kwargs["joint"])
+            #print "jointing ", e.name
+
+  def tabify(self, tabFace=None, tabDecoration=None, slotFace=None, slotDecoration=None, **kwargs):
+    for e in self.edges:
+      if e.isTab():
+        #print "tabbing ", e.name
+        for (edgename, face, length, angle, flip) in self.splitEdge(e):
+          if flip:
+            #print "-- tab on: ", edgename, face.name, angle
+            if tabDecoration is not None and ((abs(angle) > 179.5 and abs(angle) < 180.5) or tabFace is None):
+              tabDecoration(face, edgename, e.tabWidth, flip=True, **kwargs)
+            elif tabFace is not None:
+              tab = tabFace(length, e.tabWidth, **kwargs)
+              self.attachEdge(edgename, tab, tab.MAINEDGE, prefix=edgename, angle=0)
+          else:
+            #print "-- slot on: ", edgename, face.name
+            if slotFace is not None:
+              # XXX TODO: set angle appropriately
+              slot = slotFace(length, e.tabWidth, **kwargs)
+              self.attachEdge(edgename, slot, slot.MAINEDGE, prefix=edgename, angle=0)
+            if slotDecoration is not None:
+              slotDecoration(face, edgename, e.tabWidth, **kwargs)
+
+    #TODO: extend this to three+ edges
+    #component.addConnectors((conn, cname), new_edges[0], new_edges[1], depth, tabattachment=None, angle=0)
+
+  def flip(self):
+    return
+    for f in self.faces:
+      f.flip()
+
+  def transform(self, scale=1, angle=0, origin=(0,0)):
+    pass
+
+  def dotransform(self, scale=1, angle=0, origin=(0,0)):
+    for f in self.faces:
+      f.transform(scale, angle, origin)
+
+  def mirrorY(self):
+    return
+    for f in self.faces:
+      f.transform( mirrorY())
+
+  def mirrorX(self):
+    return
+    for f in self.faces:
+      f.transform( mirrorX())
+
+  def toString(self):
+    print()
+    for f in self.faces:
+      print(f.name + repr(f.edges))
+
+  def graphObj(self):
+    g = {}
+    for f in self.faces:
+      g[f.name] = dict([(e and e.name or "", e) for e in f.edges])
+    return g
+
+  def showGraph(self):
+    import objgraph
+    objgraph.show_refs(self.graphObj(), max_depth = 2, filter = lambda x : isinstance(x, (dict, HyperEdge)))
+
+  def place(self, force=False, transform3D=None):
+    if force:
+        self.unplace()
+        self.facelists = []
+
+    if transform3D is None:
+        transform3D = np.eye(4)
+
+    while True:
+        for f in self.faces:
+            if f.transform2D is not None and f.transform3D is not None:
+                continue
+            else:
+                f.place(None, None, transform3D, self.facelists)
+                break
+        else:
+            break
+
+    #IPython.embed()
+    self.rebuildEdges()
+  
+  def unplace(self):
+  
+    for f in self.faces:
+        f.transform2D = None
+        f.transform3D = None
+        
+    for e in self.edges:
+        e.pts2D = None
+        e.pts3D = None
+
+  def toSTL(self, fp, **kwargs):
+    self.place()
+    stlFaces = []
+    for face in self.faces:
+      if face.area > 0:
+        stlFaces.append([face.transform3D, face.getTriangleDict(), face.name])
+      else:
+        log.info(f"Omitting face {face.name} with area {face.area} from STL")
+    return STLWrite(stlFaces, fp, **kwargs)
+
+  '''
+  @staticmethod
+  def joinAlongEdge((g1, prefix1, edge1), (g2, prefix2, edge2), merge=True, useOrigEdge=False, angle=0):
+    # TODO(mehtank): make sure that edges are congruent
+
+    g = Graph()
+    for f in g1.faces:
+      g.addFace(f.copy(prefix(prefix1, f.name)), faceEdges = [prefix(prefix1, e.name) for e in f.edges])
+
+    return g.attach(edge1, (g2, prefix2, edge2), merge=merge, useOrigEdge=useOrigEdge, angle=angle)
+
+  @staticmethod
+  def joinAlongFace((g1, prefix1, face1), (g2, prefix2, face2), toKeep=1):
+    # TODO(mehtank): make sure that faces are congruent
+    g = Graph()
+    for f in g1.faces:
+      g.addFace(f.copy(prefix1 + "." + f.name), faceEdges = [prefix1 + "." + e.name for e in f.edges])
+    for f in g2.faces:
+      g.addFace(f.copy(prefix2 + "." + f.name), faceEdges = [prefix2 + "." + e.name for e in f.edges])
+    f1 = g.getFace(prefix1 + "." + face1)
+    f2 = g.getFace(prefix2 + "." + face2)
+    for (e1, e2) in zip(f1.edges, f2.edges):
+      e1.mergeWith(e2)
+    if toKeep < 2:
+      g.delFace(f2.name)
+    if toKeep < 1:
+      g.delFace(f1.name)
+    return g
+  '''
diff --git a/rocolib/api/composables/graph/HyperEdge.py b/rocolib/api/composables/graph/HyperEdge.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7e60a8831a2f57b43506117eea99993249c5ce3
--- /dev/null
+++ b/rocolib/api/composables/graph/HyperEdge.py
@@ -0,0 +1,179 @@
+import logging
+
+import rocolib.utils.numsym as np
+
+
+log = logging.getLogger(__name__)
+
+class HyperEdge:
+
+  #ANDYTODO: transform these into sublclasses of HyperEdge and/or componenet
+  edgeTypes = ["FOLD", "BEND", "JOINT"]
+
+  @staticmethod
+  def edge(allEdges, name, length, face, angle=0, flip=False):
+    if allEdges is not None:
+      for e in allEdges:
+        if e.name == name:
+          e.join(length, face=face, angle=angle, flip=flip)
+          return e
+
+    e = HyperEdge(name, length, face, angle, flip)
+    try:
+      allEdges.append(e)
+    except:
+      pass
+
+    return e
+
+  def __init__(self, name, length, face=None, angle=0, flip=False):
+    self.name = name
+    self.length = length
+    self.tabWidth = None
+    self.pts2D = None
+    self.pts3D = None
+    self.edgeType = "FOLD"
+    self.joint = None
+
+    #self.pt1 = pt1
+    #self.pt2 = pt2
+    if face:
+      self.faces = {face: (angle, flip)}
+    else:
+      self.faces = {}
+
+  def remove(self, face):
+    if face in self.faces:
+      self.faces.pop(face)
+      try:
+        face.disconnectFrom(self.name)
+      except (ValueError, AttributeError):
+        pass
+
+  def rename(self, name):
+    self.name = name
+
+  def isNotFlat(self):
+    return self.edgeType == "JOINT" or \
+        (self.edgeType == "FOLD" and any((x[0] for x in list(self.faces.values()))))
+
+  def isTab(self):
+    return self.tabWidth is not None
+
+  def isJoint(self):
+    return self.edgeType == "JOINT" and self.joint is not None
+
+  def setAngle(self, face, angle, flip=False):
+    if face in self.faces:
+      self.faces[face] = (angle, flip)
+
+  def getInteriorAngle(self):
+    if len(self.faces) == 1:
+      return None
+    elif len(self.faces) == 2:
+      angles = list(self.faces.values())
+      if angles[0][1]:
+        return angles[0][0] - angles[1][0]
+      else:
+        return angles[1][0] - angles[0][0]
+    else:
+      raise ValueError("Don't know how to handle edge with %d faces" % len(self.faces))
+
+  def flipConnection(self, face):
+    if face in self.faces:
+      oldangle = self.faces[face]
+      self.faces[face] = (oldangle[0], not oldangle[1])
+
+  def join(self, length, face, fromface=None, angle = 0, flip = True):
+    # angle : angle between face normals
+
+    if not self.matchesLength(length):
+      raise ValueError("Face %s of length %f cannot join edge %s of length %f." % (face.name, length, self.name, self.length))
+    
+    baseangle = 0
+    if fromface in self.faces:
+      baseangle = self.faces[fromface][0]
+    newangle = (baseangle+angle) 
+    if newangle != 180:
+      newangle = (newangle + 180 % 360) - 180
+
+    self.faces[face] = (newangle, flip)
+
+  TOL = 5e-2
+  def matchesLength(self, length):
+    try:
+        # XXX: Hack to force type error testing here
+        if (self.length - length) < self.TOL:
+            return True
+        else:
+            return False
+    except TypeError:
+        log.warning('Sympyicized variable detected in matchesLength, ignoring for now, returning True')
+        return True
+
+  def mergeWith(self, other, angle=0, flip=False, tabWidth=None):
+    # Takes all of the faces in other into self
+    if other is None:
+      return self
+    self.tabWidth = tabWidth
+    other.tabWidth = tabWidth
+
+    if not self.matchesLength(other.length):
+      raise ValueError("Edge %s of length %f cannot merge with edge %s of length %f." %
+                                (other.name, other.length, self.name, self.length))
+
+    for face in list(other.faces.keys()):
+      oldangle = other.faces[face]
+      face.replaceEdge(other, self, angle = (angle+oldangle[0]), flip = (flip ^ oldangle[1]))
+    return self
+
+  def place(self, pts2D, pts3D):
+    try:
+      if self.pts2D is not None:
+        if np.differenceExceeds(self.pts2D, pts2D, self.TOL):
+          log.warning("### Mismatched 2D transforms for edge %s " % self.name) 
+          log.warning(self.pts2D)
+          log.warning(pts2D)
+          log.warning(np.difference(self.pts2D, pts2D))
+          # raise ValueError( "Mismatched 2D transforms for edge %s " % self.name )
+      if self.pts3D is not None:
+        if np.differenceExceeds(self.pts3D, pts3D, self.TOL):
+          log.warning("### Mismatched 3D transforms for edge %s " % self.name) 
+          log.warning(self.pts3D)
+          log.warning(pts3D)
+          log.warning(np.difference(self.pts3D, pts3D))
+          # raise ValueError( "Mismatched 3D transforms for edge %s " % self.name )
+    except TypeError:
+      raise
+
+    self.pts2D = pts2D
+    self.pts3D = pts3D
+    
+  def setType(self, edgeType):
+    if edgeType is None:
+        return # do nothing
+    if edgeType not in self.edgeTypes:
+        raise Exception("Invalid edge type!")
+    self.edgeType = edgeType
+    
+  def addJoint(self, joint):
+    if not self.edgeType == "JOINT":
+        raise Exception("Trying to add joints to a non-joint edge")
+    # if not isinstance(joint, Joint.Joint):
+        # raise Exception("Not a joint!")
+    self.joint = joint
+
+  def __eq__(self, other):
+    return self.name == other.name
+
+  def __str__(self):
+    return self.name + ": " + repr(self.faces)
+
+  def __repr__(self):
+    # return self.name + " [ # faces : %d, len : %d ]" % (len(self.faces), self.length)
+    ret = "%s#%d" % (self.name, len(self.faces))
+    if len(self.faces) > 1:
+      return ret + repr(list(self.faces.values()))
+    else:
+      return ret
+
diff --git a/rocolib/api/composables/graph/Joint.py b/rocolib/api/composables/graph/Joint.py
new file mode 100644
index 0000000000000000000000000000000000000000..24cb327259e95b6938a4e43dbf118eb36a435b5b
--- /dev/null
+++ b/rocolib/api/composables/graph/Joint.py
@@ -0,0 +1,96 @@
+from rocolib.api.composables.graph.HyperEdge import HyperEdge
+import rocolib.utils.numsym as np
+
+class Joint:
+    def __init__(self, **kwargs):
+        self.kwargs = kwargs
+    def go(face, edge):
+        pass
+
+class FingerJoint(Joint):
+    def go(self, face, edge):
+        inset = False
+        edgename = face.name + edge.name
+        index = face.edgeIndex(edge.name)                                       
+        angle, flip = edge.faces[face]
+
+        thickness = self.kwargs["thickness"]
+
+        coords = face.edgeCoords(index)
+        length = face.edgeLength(index) 
+        if inset:
+            length -= thickness
+
+        pt1 = np.array(coords[0])
+        pt2 = np.array(coords[1])
+
+        n = int(max(3, round(length * 1.0 / thickness))) # number of fingers
+        dt = length * 1.0 / n                # actual thickness of fingers
+        dlp = thickness / 2. 
+        dln = thickness / 2. # Only works for 90deg angles; np.sqrt(2) max
+        dl = dlp + dln # actual length of fingers
+
+        flip = flip and (n % 2 == 1)
+
+        dpt = (pt2 - pt1) * 1.0 * dt / face.edgeLength(index)
+        ppt = np.array((dpt[1], -dpt[0])) / dt
+
+        newEdges = []
+        newPts = []
+        newPt = coords[0]
+
+        def addNew(newEdge, newPt):
+            newEdges.append(newEdge)
+            newPts.append(newPt)
+            newEdge.join(newEdge.length, face)
+
+        if inset:
+        # inset from the edge for 3 face corners
+            newPt = newPt - ppt * dln
+            newEdge = HyperEdge(edgename + "fjx1", dln)
+            addNew(newEdge, newPt)
+
+            newPt = newPt + dpt * thickness / dt / 2.0
+            newEdge = HyperEdge(edgename + "fjx2", dt)
+            addNew(newEdge, newPt)
+
+            newPt = newPt + ppt * dln
+            newEdge = HyperEdge(edgename + "fjx3", dln)
+            addNew(newEdge, newPt)
+
+        if flip:
+            newPt = newPt + ppt * dlp
+            newEdge = HyperEdge(edgename + "fj0", dlp)
+        else:
+            newPt = newPt - ppt * dln
+            newEdge = HyperEdge(edgename + "fj0", dln)
+            
+
+        for i in range(int(n)):
+            addNew(newEdge, newPt)
+
+            newPt = newPt + dpt 
+            newEdge = HyperEdge(edgename + "fjd%d" % i, dt)
+            addNew(newEdge, newPt)
+
+            if flip:
+                newPt = newPt - ppt * dl
+            else:
+                newPt = newPt + ppt * dl
+            newEdge = HyperEdge(edgename + "fjp%d" % i, dl)
+            flip = not flip
+
+        if not flip:
+            addNew(newEdge, newPt)
+        else:
+            newPt = newPt - ppt * dl
+
+        if inset:
+            newPt = newPt + dpt * thickness / dt / 2.0
+            newEdge = HyperEdge(edgename + "fjy", dt)
+            addNew(newEdge, newPt)
+
+        newEdge = HyperEdge(edgename + "fjn", dln)
+        newEdges.append(newEdge)
+
+        return newPts, newEdges
diff --git a/rocolib/api/composables/graph/__init__.py b/rocolib/api/composables/graph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/rocolib/api/composables/graph/stlwriter.py b/rocolib/api/composables/graph/stlwriter.py
new file mode 100644
index 0000000000000000000000000000000000000000..96b82b779bd8106e327c307fcb1478f1d29b5105
--- /dev/null
+++ b/rocolib/api/composables/graph/stlwriter.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+#coding:utf-8
+# Purpose: Export 3D objects, build of faces with 3 or 4 vertices, as ASCII or Binary STL file.
+# License: MIT License
+
+import struct
+
+ASCII_FACET = """facet normal 0 0 0
+outer loop
+vertex {face[0][0]:.4f} {face[0][1]:.4f} {face[0][2]:.4f}
+vertex {face[1][0]:.4f} {face[1][1]:.4f} {face[1][2]:.4f}
+vertex {face[2][0]:.4f} {face[2][1]:.4f} {face[2][2]:.4f}
+endloop
+endfacet
+"""
+
+BINARY_HEADER ="80sI"
+BINARY_FACET = "12fH"
+
+class ASCII_STL_Writer:
+    """ Export 3D objects build of 3 or 4 vertices as ASCII STL file.
+    """
+    def __init__(self, stream):
+        self.fp = stream
+        self._write_header()
+
+    def _write_header(self):
+        self.fp.write("solid python\n")
+
+    def close(self):
+        self.fp.write("endsolid python\n")
+
+    def _write(self, face):
+        self.fp.write(ASCII_FACET.format(face=face))
+
+    def _split(self, face):
+        p1, p2, p3, p4 = face
+        return (p1, p2, p3), (p3, p4, p1)
+
+    def add_face(self, face):
+        """ Add one face with 3 or 4 vertices. """
+        pt1 = tuple(face[:3,0])
+        for i in range(2, len(face[1,:])):
+          pt2 = tuple(face[:3,i-1])
+          pt3 = tuple(face[:3,i])
+          self._write((pt1, pt2, pt3))
+
+    def add_faces(self, faces):
+        """ Add many faces. """
+        for face in faces:
+            self.add_face(face)
+
+class Binary_STL_Writer(ASCII_STL_Writer):
+    """ Export 3D objects build of 3 or 4 vertices as binary STL file.
+    """
+    def __init__(self, stream):
+        self.counter = 0
+        ASCII_STL_Writer.__init__(self, stream)
+
+    def close(self):
+        self._write_header()
+
+    def _write_header(self):
+        self.fp.seek(0)
+        self.fp.write(struct.pack(BINARY_HEADER, b'Python Binary STL Writer', self.counter))
+
+    def _write(self, face):
+        self.counter += 1
+        data = [
+            0., 0., 0.,
+            face[0][0], face[0][1], face[0][2],
+            face[1][0], face[1][1], face[1][2],
+            face[2][0], face[2][1], face[2][2],
+            0
+        ]
+        self.fp.write(struct.pack(BINARY_FACET, *data))
+
+
+def example():
+    def get_cube():
+        # cube corner points
+        s = 3.
+        p1 = (0, 0, 0)
+        p2 = (0, 0, s)
+        p3 = (0, s, 0)
+        p4 = (0, s, s)
+        p5 = (s, 0, 0)
+        p6 = (s, 0, s)
+        p7 = (s, s, 0)
+        p8 = (s, s, s)
+
+        # define the 6 cube faces
+        # faces just lists of 3 or 4 vertices
+        return [
+            [p1, p5, p7, p3],
+            [p1, p5, p6, p2],
+            [p5, p7, p8, p6],
+            [p7, p8, p4, p3],
+            [p1, p3, p4, p2],
+            [p2, p6, p8, p4],
+        ]
+
+    with open('cube.stl', 'wb') as fp:
+        writer = Binary_STL_Writer(fp)
+        writer.add_faces(get_cube())
+        writer.close()
+
+if __name__ == '__main__':
+    example()
+
diff --git a/rocolib/api/ports/AnchorPort.py b/rocolib/api/ports/AnchorPort.py
new file mode 100644
index 0000000000000000000000000000000000000000..632d5f9b74667757948bb0c3083a4e005453d084
--- /dev/null
+++ b/rocolib/api/ports/AnchorPort.py
@@ -0,0 +1,27 @@
+from rocolib.api.ports import Port
+from rocolib.utils.utils import prefix as prefixString
+
+class AnchorPort(Port):
+  def __init__(self, parent, graph, face, transform):
+    Port.__init__(self, parent, {})
+    self.graph = graph
+    self.face = face
+    self.transform = transform
+
+  def prefix(self, prefix=""):
+    self.face = prefixString(prefix, self.face)
+
+  def getFace(self):
+    return self.graph.getFace(self.face)
+
+  def getTransform(self):
+    return self.transform
+
+  def getFaceName(self):
+    return self.face
+
+  def toString(self):
+    return str(self.face.name)
+
+  def canMate(self, otherPort):
+    return False
diff --git a/rocolib/api/ports/EdgePort.py b/rocolib/api/ports/EdgePort.py
new file mode 100644
index 0000000000000000000000000000000000000000..365cbe29c3c49d5d55a5c73f2e4dd2701a0dc20a
--- /dev/null
+++ b/rocolib/api/ports/EdgePort.py
@@ -0,0 +1,32 @@
+from rocolib.api.ports import Port
+from rocolib.utils.utils import prefix as prefixString
+
+class EdgePort(Port):
+  def __init__(self, parent, edges, lengths):
+    Port.__init__(self, parent, {})
+    if isinstance(edges, str) or not hasattr(edges, "__iter__"):
+        edges = [edges]
+    if isinstance(lengths, str) or not hasattr(lengths, "__iter__"):
+        lengths = [lengths]
+    if len(edges) != len(lengths):
+        raise ValueError("Need one-to-one mapping of edges to lengths")
+    self.edges = edges
+    self.lengths = lengths
+
+  def getEdges(self):
+    return self.edges
+
+  def getParams(self):
+    return self.lengths
+
+  def prefix(self, prefix=""):
+    self.edges = [prefixString(prefix, e) for e in self.edges]
+
+  def canMate(self, otherPort):
+    try:
+      return len(self.getEdges()) == len(otherPort.getEdges())
+    except AttributeError:
+      return False
+
+  def toString(self):
+    return str(self.getEdges())
diff --git a/rocolib/api/ports/FacePort.py b/rocolib/api/ports/FacePort.py
new file mode 100644
index 0000000000000000000000000000000000000000..e95c00597b84f4e0426d2330c79b400a253b1e4f
--- /dev/null
+++ b/rocolib/api/ports/FacePort.py
@@ -0,0 +1,32 @@
+from rocolib.api.ports import Port
+from rocolib.utils.utils import prefix as prefixString
+
+class FacePort(Port):
+  def __init__(self, parent, graph, face):
+    Port.__init__(self, parent, {})
+    self.graph = graph
+    self.face = face
+
+  def prefix(self, prefix=""):
+    self.face = prefixString(prefix, self.face)
+
+  def getFace(self):
+    return self.graph.getFace(self.face)
+
+  def getFaceName(self):
+    return self.face
+
+  def toString(self):
+    return str(self.face.name)
+
+  def canMate(self, otherPort):
+    try:
+      if (otherPort.getDecoration() is not None):
+        return True
+    except AttributeError:
+      pass
+    try:
+      if (otherPort.getFaceName() is not None):
+        return True
+    except:
+      pass
diff --git a/rocolib/api/ports/MountPort.py b/rocolib/api/ports/MountPort.py
new file mode 100644
index 0000000000000000000000000000000000000000..179e2ab5aca837dcdac1a3d03eb43382eba967cc
--- /dev/null
+++ b/rocolib/api/ports/MountPort.py
@@ -0,0 +1,16 @@
+from rocolib.api.ports import Port
+from rocolib.utils.utils import prefix as prefixString
+
+class MountPort(Port):
+  def __init__(self, parent, decoration):
+    Port.__init__(self, parent, {})
+    self.decoration = decoration
+
+  def getDecoration(self):
+    return self.decoration
+
+  def toString(self):
+    print("decoration")
+
+  def canMate(self, otherPort):
+    return (otherPort.getFaceName() is not None)
diff --git a/rocolib/api/ports/Port.py b/rocolib/api/ports/Port.py
new file mode 100644
index 0000000000000000000000000000000000000000..6bc3e0ac4babe4afc1404f2d1c3046368b6df38a
--- /dev/null
+++ b/rocolib/api/ports/Port.py
@@ -0,0 +1,160 @@
+from rocolib.api.Parameterized import Parameterized
+
+
+class Port(Parameterized):
+  """
+  Abstract base class for a Port
+  """
+  def __init__(self, parent, params, name='', **kwargs):
+    """
+    :param parent: component that is holding this port
+    :type parent: component
+    :param params: parameters to initialize the Parameterized parameters with
+    :type params: dict
+    :param name: port name
+    :type name: basestring
+    :param kwargs: additional arguments to override params
+    :type kwargs: dict
+    """
+    super(Port, self).__init__()
+
+    # XXX TODO(mehtank): Figure out better default values
+    self.isInput = False # True if self.valueFunction can be set via a connection from a port of a different component 
+    self.isOutput = False # True if self.valueFunction can be completely determined by self.parent
+    self.valueFunction = None 
+    # self.inputFunction = None
+    # self.outputFunction = None
+
+    self.parent = parent
+    self._allowableMates = []
+    self._recommendedMates = []
+    self.setName(name)
+
+    for key, value in params.items():
+      self.addParameter(key, value)
+
+    for key, value in kwargs.items():
+      self.setParameter(key, value)
+
+
+  def inherit(self, parent, component):
+    # Override to handle getting inherited to parent.interfaces[name]
+    pass
+
+  def prefix(self, prefix=""):
+    # Override to handle prefixing
+    pass
+
+  def setInputValue(self, value):
+    self.isInput = True
+    self.isOutput = False
+    self.valueFunction = lambda : value
+
+  def setOutputFunction(self, fn):
+    self.isInput = False
+    self.isOutput = True
+    self.valueFunction = fn
+
+  def setDrivenFunction(self, fn):
+    self.isInput = False
+    self.isOutput = False
+    self.valueFunction = fn
+
+  def getValue(self, default=None):
+    if self.valueFunction is None:
+      return default
+    return self.valueFunction()
+
+  def canMate(self, otherPort):
+    """
+    If _allowableMates is an empty list, then returns if self and otherPort
+    are the same class.  Otherwise, return if otherPort is an instance of
+    any of _allowableMates
+
+    Override this method for better matching
+    :returns: whether this port can mate with another port
+    :rtype: boolean
+    """
+    if len(self._allowableMates) > 0:
+      for nextType in self._allowableMates:
+        if isinstance(otherPort, nextType):
+          return True
+      return False
+    return self.__class__ == otherPort.__class__
+
+
+  def shouldMate(self, otherPort):
+    # Override for better matching
+    if not self.canMate(otherPort):
+      return False
+    if len(self._recommendedMates) > 0:
+      for nextType in self._recommendedMates:
+        if isinstance(otherPort, nextType):
+          return True
+    return False
+
+
+  def addAllowableMate(self, mateType):
+    if not isinstance(mateType, (list, tuple)):
+      mateType = [mateType]
+    for newType in mateType:
+      # XXX what exactly does this check?
+      if not isinstance(newType, type(self.__class__)):
+          continue
+      # If already have one that is a subclass of the desired one, do nothing
+      for mate in self._allowableMates:
+        if issubclass(mate, newType):
+          # XXX why do we return instead of breaking and checking the rest?
+          return
+      # Remove any that are a superclass of the new one
+      for mate in self._allowableMates:
+        if issubclass(newType, mate):
+          self._allowableMates.remove(mate)
+      self._allowableMates.append(newType)
+
+
+  def addRecommendedMate(self, mateType):
+    if not isinstance(mateType, (list, tuple)):
+      mateType = [mateType]
+    for newType in mateType:
+      if not isinstance(newType, type(self.__class__)):
+          continue
+      # If already have one that is a subclass of the desired one, do nothing
+      for mate in self._recommendedMates:
+        if issubclass(mate, newType):
+          return None
+      # Remove any that are a superclass of the new one
+      for mate in self._recommendedMates:
+        if issubclass(newType, mate):
+          self._recommendedMates.remove(mate)
+      self._recommendedMates.append(newType)
+
+
+  def setParent(self, newParent):
+    self.parent = newParent
+
+
+  def getParent(self):
+    return self.parent
+
+
+  def setName(self, name):
+    self.name = str(name)
+
+
+  def getName(self):
+    return self.name
+
+
+  def toString(self):
+    return str(self.parent) + '.' + self.getName()
+
+
+  def getCompatiblePorts(self):
+    from rocolib.api.ports import all_ports
+    compat_ports = []
+    for port in all_ports:
+        if self.canMate(port(None)):
+            compat_ports.append(port)
+    return compat_ports
+
diff --git a/rocolib/api/ports/__init__.py b/rocolib/api/ports/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4107d13b37424bdc5b3a88a5ef8dba42b09cf6a8
--- /dev/null
+++ b/rocolib/api/ports/__init__.py
@@ -0,0 +1,48 @@
+from .Port import Port
+
+from .EdgePort import EdgePort
+from .FacePort import FacePort
+from .MountPort import MountPort
+from .AnchorPort import AnchorPort
+
+from .ElectricalPort import ElectricalPort
+from .ElectricalInputPort import ElectricalInputPort
+from .ElectricalOutputPort import ElectricalOutputPort
+from .PowerInputPort import PowerInputPort
+from .PowerOutputPort import PowerOutputPort
+from .SerialTXPort import SerialTXPort
+from .SerialRXPort import SerialRXPort
+from .PWMInputPort import PWMInputPort
+from .PWMOutputPort import PWMOutputPort
+from .ServoInputPort import ServoInputPort
+from .ServoOutputPort import ServoOutputPort
+from .AnalogInputPort import AnalogInputPort
+from .AnalogOutputPort import AnalogOutputPort
+from .DigitalInputPort import DigitalInputPort
+from .DigitalOutputPort import DigitalOutputPort
+from .OneWireSerialPort import OneWireSerialPort
+
+from .DataPort import DataPort
+from .DataOutputPort import DataOutputPort
+from .DataInputPort import DataInputPort
+
+all_ports = [
+    ElectricalInputPort,
+    ElectricalOutputPort,
+    PowerInputPort,
+    PowerOutputPort,
+    SerialTXPort,
+    SerialRXPort,
+    PWMInputPort,
+    PWMOutputPort,
+    ServoInputPort,
+    ServoOutputPort,
+    AnalogInputPort,
+    AnalogOutputPort,
+    DigitalInputPort,
+    DigitalOutputPort,
+    OneWireSerialPort,
+    DataInputPort,
+    DataOutputPort,
+]
+
diff --git a/rocolib/builders/BoatBaseBuilder.py b/rocolib/builders/BoatBaseBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..6007f07dbffb1fd073dcdbea4a2af8425e6009fd
--- /dev/null
+++ b/rocolib/builders/BoatBaseBuilder.py
@@ -0,0 +1,15 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("boat","SimpleUChannel", inherit=True)
+c.addSubcomponent("bow","BoatPoint", inherit=True)
+c.addSubcomponent("stern","BoatPoint", inherit=True)
+
+c.join(("boat", "top"), ("bow", "edge"))
+c.join(("boat", "bot"), ("stern", "edge"))
+
+c.inheritInterface("portedge", ("boat", "ledge"))
+c.inheritInterface("staredge", ("boat", "redge"))
+
+c.toLibrary("BoatBase")
diff --git a/rocolib/builders/CabinBuilder.py b/rocolib/builders/CabinBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d4a19d79947b189caa5e251a67e47d922442908
--- /dev/null
+++ b/rocolib/builders/CabinBuilder.py
@@ -0,0 +1,61 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+# BOX
+
+c.addParameter("depth", 50, paramType="length")
+c.addParameter("width", 60, paramType="length")
+c.addParameter("height", 30, paramType="length")
+
+c.addSubcomponent("top","Rectangle")
+c.addSubcomponent("fore","Rectangle")
+c.addSubcomponent("rear","Rectangle")
+c.addSubcomponent("port","Rectangle")
+c.addSubcomponent("star","Rectangle")
+
+c.addConstraint(("top","w"), "depth")
+c.addConstraint(("top","l"), "width")
+
+c.addConstraint(("fore","w"), "height")
+c.addConstraint(("fore","l"), "width")
+c.addConstraint(("rear","w"), "height")
+c.addConstraint(("rear","l"), "width")
+
+c.addConnection(("top", "b"), ("rear", "t"), angle=90)
+c.addConnection(("top", "t"), ("fore", "b"), angle=90)
+
+c.addConstraint(("port","w"), "depth")
+c.addConstraint(("port","l"), "height")
+c.addConstraint(("star","w"), "depth")
+c.addConstraint(("star","l"), "height")
+
+c.addConnection(("top", "l"), ("port", "r"), angle=90)
+c.addConnection(("top", "r"), ("star", "l"), angle=90)
+
+c.addConnection(("port", "t"), ("fore", "l"), angle=90, tabWidth=10)
+c.addConnection(("fore", "r"), ("star", "t"), angle=90, tabWidth=10)
+c.addConnection(("star", "b"), ("rear", "r"), angle=90, tabWidth=10)
+c.addConnection(("port", "b"), ("rear", "l"), angle=90, tabWidth=10)
+
+# Interface to floats
+
+c.addParameter("length", 200, paramType="length")
+
+c.addSubcomponent("portsplit","SplitEdge")
+c.addSubcomponent("starsplit","SplitEdge")
+
+c.addConstraint(("portsplit","botlength"), ("length", "depth"), "[sum(x)]")
+c.addConstraint(("portsplit","toplength"), ("length", "depth"), "[x[0]/2., x[1], x[0]/2.]")
+c.addConnection(("portsplit", "topedge1"), ("port", "l"))
+
+c.addConstraint(("starsplit","botlength"), ("length", "depth"), "[sum(x)]")
+c.addConstraint(("starsplit","toplength"), ("length", "depth"), "[x[0]/2., x[1], x[0]/2.]")
+c.addConnection(("starsplit", "topedge1"), ("star", "r"))
+
+c.inheritInterface("portedge", ("portsplit", "botedge0"))
+c.inheritInterface("staredge", ("starsplit", "botedge0"))
+c.inheritInterface("foreedge", ("fore", "t"))
+c.inheritInterface("rearedge", ("rear", "b"))
+
+c.toLibrary("Cabin")
diff --git a/rocolib/builders/CanoeBuilder.py b/rocolib/builders/CanoeBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea203e8063399eddc3ca92ce3e6fbf174563f025
--- /dev/null
+++ b/rocolib/builders/CanoeBuilder.py
@@ -0,0 +1,30 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+# BOX
+
+c.addSubcomponent("boat","BoatBase", inherit=True, prefix=None, root=True)
+
+c.addParameter("seats", 3, paramType="count", minValue=1, maxValue=10)
+
+c.addSubcomponent("portsplit","SplitEdge")
+c.addSubcomponent("starsplit","SplitEdge")
+
+c.addConstraint(("portsplit","botlength"), ("boat.length", "seats"), "(x[0],)")
+c.addConstraint(("portsplit","toplength"), ("boat.length", "seats"), "(x[0]/(2.*x[1]+1.),) * (2*x[1]+1)")
+c.addConstraint(("starsplit","toplength"), ("boat.length", "seats"), "(x[0],)")
+c.addConstraint(("starsplit","botlength"), ("boat.length", "seats"), "(x[0]/(2.*x[1]+1.),) * (2*x[1]+1)")
+
+c.addConnection(("portsplit", "botedge0"), ("boat", "portedge"), angle=90)
+c.addConnection(("starsplit", "topedge0"), ("boat", "staredge"), angle=90, tabWidth=10)
+
+for i in range(10):
+    nm = "seat%d"%i
+    c.addSubcomponent(nm, "Rectangle")
+    c.addConstraint((nm, "l"), ("boat.width", "seats"), "(%d < x[1]) and x[0] or 0" % i)
+    c.addConstraint((nm, "w"), ("boat.length", "seats"), "x[0]/(2.*x[1]+1.)")
+    c.addConnection(("portsplit", "topedge%d" % (2*i+1)), (nm, "l"))
+    c.addConnection(("starsplit", "botedge%d" % (2*i+1)), (nm, "r"))
+
+c.toLibrary("Canoe")
diff --git a/rocolib/builders/CatBuilder.py b/rocolib/builders/CatBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d3b16c90ac1d1826e04eef16000fa10e8101e5f
--- /dev/null
+++ b/rocolib/builders/CatBuilder.py
@@ -0,0 +1,31 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+# BOX
+
+c.addSubcomponent("cabin","Cabin", inherit=True, prefix=None)
+c.addSubcomponent("port","BoatBase", root=True)
+c.addSubcomponent("star","BoatBase")
+
+c.addConstraint(("port","boat.length"), ("length", "depth"), "sum(x)")
+c.addConstraint(("port","boat.width"), "width", "x/4.")
+c.addConstraint(("port","boat.depth"), ("length", "depth"), "sum(x)/20.")
+c.addConstraint(("port","bow.point"), "length", "x/2.")
+c.addConstraint(("port","stern.point"), "length", "x/8.")
+
+c.addConstraint(("star","boat.length"), ("length", "depth"), "sum(x)")
+c.addConstraint(("star","boat.width"), "width", "x/4.")
+c.addConstraint(("star","boat.depth"), ("length", "depth"), "sum(x)/20.")
+c.addConstraint(("star","bow.point"), "length", "x/2.")
+c.addConstraint(("star","stern.point"), "length", "x/8.")
+
+c.addConnection(("cabin", "portedge"), ("port", "portedge"))
+c.addConnection(("cabin", "staredge"), ("star", "staredge"))
+
+c.inheritInterface("foreedge", ("cabin", "foreedge"))
+c.inheritInterface("rearedge", ("cabin", "rearedge"))
+c.inheritInterface("portedge", ("port", "staredge"))
+c.inheritInterface("staredge", ("star", "portedge"))
+
+c.toLibrary("Catamaran")
diff --git a/rocolib/builders/CatFoilBuilder.py b/rocolib/builders/CatFoilBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..407e0cc294b5e3cbca915f6f7e1d68cb1b2a0dcc
--- /dev/null
+++ b/rocolib/builders/CatFoilBuilder.py
@@ -0,0 +1,25 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+# BOX
+
+c.addSubcomponent("boat","Catamaran", inherit=True, prefix=None, root=True)
+c.addSubcomponent("port","Foil", inherit=True, prefix=None)
+c.addSubcomponent("star","Foil", inherit=True, prefix=None)
+
+c.delParameter("flip")
+
+c.addConstraint(("port","width"), "width", "x/4.")
+c.addConstraint(("port","height"), ("length", "depth"), "sum(x)/3.")
+c.addConstConstraint(("port","flip"), True)
+
+c.addConstraint(("star","width"), "width", "x/4.")
+c.addConstraint(("star","height"), ("length", "depth"), "sum(x)/3.")
+c.addConstConstraint(("star","flip"), False)
+
+c.addConnection(("boat", "portedge"), ("port", "mount"), angle=-180)
+c.addConnection(("boat", "staredge"), ("star", "mount"), angle=-180)
+c.addConnection(("star", "join"), ("port", "join"), tabWidth=10)
+
+c.toLibrary("CatFoil")
diff --git a/rocolib/builders/ChairBackBuilder.py b/rocolib/builders/ChairBackBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..03e7c254023c2a537682864ee5e1687723adbc67
--- /dev/null
+++ b/rocolib/builders/ChairBackBuilder.py
@@ -0,0 +1,24 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("panel","ChairPanel", inherit=("width", "thickness"), prefix=None)
+
+c.addParameter("gapheight", 20, paramType="length")
+c.addParameter("backheight", 40, paramType="length")
+
+c.addConstraint(("panel","depth"), "backheight")
+
+c.addSubcomponent("sider","Rectangle")
+c.addConstraint(("sider","l"), "thickness")
+c.addConstraint(("sider","w"), "gapheight")
+c.addConnection(("panel","tr"),("sider","t"), angle=0)
+c.inheritInterface("right", ("sider", "b"))
+
+c.addSubcomponent("sidel","Rectangle")
+c.addConstraint(("sidel","l"), "thickness")
+c.addConstraint(("sidel","w"), "gapheight")
+c.addConnection(("panel","tl"),("sidel","t"), angle=0)
+c.inheritInterface("left", ("sidel", "b"))
+
+c.toLibrary("ChairBack")
diff --git a/rocolib/builders/ChairPanelBuilder.py b/rocolib/builders/ChairPanelBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..2228f4cfb17073fc8c29b2dd2901366cb9133dd4
--- /dev/null
+++ b/rocolib/builders/ChairPanelBuilder.py
@@ -0,0 +1,31 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("back","Rectangle", root=True)
+c.addSubcomponent("sidel","Rectangle")
+c.addSubcomponent("sider","Rectangle")
+
+c.addParameter("depth", 50, paramType="length")
+c.addParameter("width", 70, paramType="length")
+c.addParameter("thickness", 10, paramType="length")
+
+c.addConstraint(("back","l"), "width")
+c.addConstraint(("back","w"), "depth")
+
+c.addConstraint(("sidel","l"), "thickness")
+c.addConstraint(("sidel","w"), "depth")
+c.addConstraint(("sider","l"), "thickness")
+c.addConstraint(("sider","w"), "depth")
+
+c.addConnection(("sidel","r"),("back","l"), angle=90)
+c.addConnection(("back","r"),("sider","l"), angle=90)
+
+c.inheritInterface("tr", ("sider", "t"))
+c.inheritInterface("tl", ("sidel", "t"))
+c.inheritInterface("br", ("sider", "b"))
+c.inheritInterface("bl", ("sidel", "b"))
+c.inheritInterface("right", ("sider", "r"))
+c.inheritInterface("left", ("sidel", "l"))
+
+c.toLibrary("ChairPanel")
diff --git a/rocolib/builders/ChairSeatBuilder.py b/rocolib/builders/ChairSeatBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..785557fcd6ccb29ebde23a61175bbf80d9f75082
--- /dev/null
+++ b/rocolib/builders/ChairSeatBuilder.py
@@ -0,0 +1,24 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("seat","ChairPanel", inherit=True, prefix=None, root=True)
+c.addSubcomponent("back","ChairBack", inherit=True, prefix=None)
+c.addSubcomponent("kitel","Kite", inherit="thickness", prefix=None)
+c.addSubcomponent("kiter","Kite", inherit="thickness", prefix=None)
+
+c.addParameter("recline", 110, paramType="angle")
+
+c.addConstraint(("kitel","angle"), "recline", "180 - x")
+c.addConstraint(("kiter","angle"), "recline", "180 - x")
+
+c.addConnection(("back","left"),("kitel","t"), angle=0)
+c.addConnection(("seat","bl"),("kitel","b"), angle=0)
+
+c.addConnection(("back","right"),("kiter","b"), angle=0)
+c.addConnection(("seat","br"),("kiter","t"), angle=0)
+
+c.inheritInterface("right", ("seat", "right"))
+c.inheritInterface("left", ("seat", "left"))
+
+c.toLibrary("ChairSeat")
diff --git a/rocolib/builders/ESPBrainsBuilder.py b/rocolib/builders/ESPBrainsBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..0071a1dac000374f6de23bc8e369a9b0ff846dea
--- /dev/null
+++ b/rocolib/builders/ESPBrainsBuilder.py
@@ -0,0 +1,47 @@
+from rocolib.api.components.Component import Component
+from rocolib.api.Function import Function
+
+
+self = Component()
+
+self.addSubcomponent("beam", "RectBeam")
+self.addSubcomponent("header", "Header")
+self.addSubcomponent("servoPins", "Cutout")
+
+self.addParameter("brain", "nodeMCU", paramType="dimension")
+self.addParameter("length", 90, paramType="length")
+self.addParameter("width", 10, paramType="length")
+self.addParameter("depth", 10, paramType="length")
+
+### Set specific relationships between parameters
+def getBrainParameter(p):
+  return "brain", "getDim(x, '%s')" % p
+
+self.addConstraint(("beam", "width"), "depth")
+self.addConstraint(("beam", "depth"), "width")
+self.addConstraint(("beam", "length"), "length")
+self.addConstConstraint(("beam", "angle"), 90)
+
+self.addConstraint(("beam", "minwidth"), *getBrainParameter("height"))
+self.addConstraint(("beam", "mindepth"), *getBrainParameter("width"))
+self.addConstraint(("beam", "minlength"), *getBrainParameter("length"))
+
+self.addConstraint(("header", "nrows"), *getBrainParameter("nrows"))
+self.addConstraint(("header", "ncols"), *getBrainParameter("ncols"))
+self.addConstraint(("header", "rowsep"), *getBrainParameter("rowsep"))
+self.addConstraint(("header", "colsep"), *getBrainParameter("colsep"))
+
+self.addConstConstraint(("servoPins", "dy"), 8)
+self.addConstConstraint(("servoPins", "dx"), 27)
+
+self.addConnection(("beam", "face1"),
+                   ("header", "decoration"),
+                   mode="hole", offset=Function(params=("length", "brain"), fnstring="(7.5, -4.5 + 0.5*(x[0]-getDim(x[1], 'length')))"))
+
+self.addConnection(("beam", "face1"),
+                   ("servoPins", "decoration"),
+                   mode="hole", rotate=True, offset=Function(params=("length", "brain"), fnstring="(-17.25, (0.5 * (x[0]-getDim(x[1], 'length')))-14)"))
+
+self.inheritAllInterfaces("beam", prefix=None)
+
+self.toLibrary("ESPBrains")
diff --git a/rocolib/builders/ESPSegBuilder.py b/rocolib/builders/ESPSegBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..068cad9a3ca16bd3845f2c0d74f3d4c849e4d98b
--- /dev/null
+++ b/rocolib/builders/ESPSegBuilder.py
@@ -0,0 +1,110 @@
+from rocolib.api.components.Component import Component
+from rocolib.api.Function import Function
+
+c = Component()
+
+c.addParameter("length", 90, paramType="length")
+c.addParameter("width", 60, paramType="length")
+c.addParameter("height", 40, paramType="length")
+
+c.addParameter("controller", "nodeMCU", paramType="dimension")
+c.addParameter("driveservo", "fs90r", paramType="dimension")
+
+c.addSubcomponent("brain", "ESPBrains")
+c.addSubcomponent("right", "Wheel", invert=True)
+c.addSubcomponent("left", "Wheel", invert=True)
+
+# Constant thickness of main body
+def depthfn(params = None, fnmod = None):
+    if params is None: params = []
+    if fnmod is None: fnmod = "%s"
+    return ["controller", "driveservo"] + params, \
+            fnmod % "max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))"
+
+# Set microcontroller
+c.addConstraint(("brain", "depth"), *depthfn())
+c.addConstraint(("brain", "length"), "width")
+c.addConstraint(("brain", "brain"), "controller")
+
+for servo in ("right", "left"):
+    c.addConstraint((servo,"depth"), *depthfn())
+    c.addConstraint((servo, "length"), 
+                    ("length", "controller"), 
+                    "x[0] - getDim(x[1],'width')")
+    c.addConstConstraint((servo, "center"), False)
+    c.addConstraint((servo, "servo"), "driveservo")
+    c.addConstConstraint((servo, "angle"), 90)
+    c.addConstraint((servo, "radius"), "height")
+
+c.addConstConstraint(("left","phase"), 2)
+
+# connections
+c.addConnection(("brain", "topedge0"),
+                ("right", "topedge2"),
+                angle=-90)
+c.addConnection(("brain", "botedge0"),
+                ("left", "topedge0"),
+                angle=-90)
+
+# Sheath 
+c.addParameter("battery", 7, paramType="length")
+
+c.addSubcomponent("sheath", "RectBeam")
+c.addConstConstraint(("sheath","phase"), 1)
+c.addConstraint(("sheath","length"), "length")
+c.addConstraint(("sheath","width"), "width")
+c.addConstraint(("sheath","depth"), *depthfn(["battery"], "%s + x[2]"))
+c.addConstConstraint(("sheath","angle"), 90)
+
+c.addSubcomponent("sheathsplit", "SplitEdge")
+c.addConstraint(("sheathsplit","toplength"), "width", "(x,)")
+c.addConstraint(("sheathsplit","botlength"), ("driveservo", "width"), 
+        "(getDim(x[0],'motorheight'), \
+          x[1] - 2*getDim(x[0],'motorheight'), \
+          getDim(x[0],'motorheight'))")
+
+c.addConnection(("left", "botedge1"),
+                ("sheathsplit", "botedge2"),
+                angle=180)
+
+'''
+c.addConnection(("right", "botedge1"),
+                ("sheathsplit", "botedge0"),
+                angle=180, tabWidth=40)
+'''
+
+c.addConnection(("sheathsplit", "topedge0"),
+                ("sheath", "topedge1"))
+
+# Tail
+c.addSubcomponent("tail", "Tail", inherit=("flapwidth", "tailwidth"), prefix=None)
+c.addConstraint(("tail","width"), "width")
+c.addConstraint(("tail","height"), *depthfn(["height"], "%s/2.+x[2]"))
+c.addConstraint(("tail","depth"), *depthfn(["battery"], "%s+x[2]"))
+
+c.addConnection(("tail", "topedge"),
+                ("sheath", "botedge1"),
+                angle=90)
+
+c.addSubcomponent("tailsplit", "SplitEdge")
+c.addConstraint(("tailsplit","toplength"), "width", "(x,)")
+c.addConstraint(("tailsplit","botlength"), ("width", "flapwidth"), "(x[0]*(1-x[1])/2., x[0]*x[1], x[0]*(1-x[1])/2.)")
+
+c.addConnection(("sheath", "botedge3"),
+                ("tailsplit", "topedge0"))
+
+c.addConnection(("tail", "flapedge"),
+                ("tailsplit", "botedge1"),
+                angle=90, tabWidth=Function(*depthfn(["battery"], "%s+x[2]")))
+
+# USB charging port for battery
+c.addSubcomponent("usb", "Cutout")
+c.addConstConstraint(("usb", "dy"), 9)
+c.addConstConstraint(("usb", "dx"), 4)
+
+c.addConnection(("sheath", "face1"),
+                ("usb", "decoration"),
+                mode="hole",
+                offset=Function(*depthfn(["length", "battery"], "(4-(%s+x[3])/2, 0.5 * x[2] - 15)")))
+
+c.toLibrary("ESPSeg")
diff --git a/rocolib/builders/FoilBuilder.py b/rocolib/builders/FoilBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..dcfcf6560ce2556b18f6e907b4b8ccd87fa96669
--- /dev/null
+++ b/rocolib/builders/FoilBuilder.py
@@ -0,0 +1,36 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addParameter("height", 100, paramType="length")
+c.addParameter("length", 40, paramType="length")
+c.addParameter("depth", 20, paramType="length")
+c.addParameter("dl", 0.1, paramType="length")
+c.addParameter("width", 10, paramType="length")
+c.addParameter("flip", False, valueType="bool")
+
+c.addSubcomponent("stick", "Rectangle")
+c.addSubcomponent("foil","Wing")
+c.addSubcomponent("split","SplitEdge")
+
+c.addConstraint(("split","botlength"), ("length", "depth"), "[sum(x)]")
+c.addConstraint(("split","toplength"), ("length", "depth"), "[x[0]/2., x[1], x[0]/2.]")
+
+c.addConstraint(("stick","l"), "height")
+c.addConstraint(("stick","w"), "depth")
+
+c.addConstraint(("foil","bodylength"), "depth")
+c.addConstraint(("foil","wingtip"), "depth")
+c.addConstraint(("foil","thickness"), ("dl", "depth"), "x[0] * x[1]")
+c.addConstraint(("foil","wingspan"), "width")
+c.addConstraint(("foil","flip"), "flip")
+
+
+c.addConnection(("split", "topedge1"), ("stick", "l"))
+c.addConnection(("stick", "r"), ("foil", "tip"), angle=90)
+
+
+c.inheritInterface("mount", ("split", "botedge0"))
+c.inheritInterface("join", ("foil", "base"))
+
+c.toLibrary("Foil")
diff --git a/rocolib/builders/MountedServoBuilder.py b/rocolib/builders/MountedServoBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1e8fda87d574025130b1e8cf0248460efd56b32
--- /dev/null
+++ b/rocolib/builders/MountedServoBuilder.py
@@ -0,0 +1,14 @@
+from rocolib.api.components.Component import Component
+from rocolib.api.Function import Function
+
+c = Component()
+
+c.addSubcomponent("mount", "ServoMount", inherit=True, prefix=None)
+c.addSubcomponent("servo", "ServoMotor", inherit=True, prefix=None)
+
+c.inheritAllInterfaces("mount", prefix=None)
+c.inheritAllInterfaces("servo", prefix=None)
+c.addConnection(("mount", "mount.decoration"),
+                ("servo", "horn"))
+
+c.toLibrary("MountedServo")
diff --git a/rocolib/builders/PaperbotBuilder.py b/rocolib/builders/PaperbotBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..1bd69fa8cd07c1719d8e030a496d61ad4071c3dc
--- /dev/null
+++ b/rocolib/builders/PaperbotBuilder.py
@@ -0,0 +1,11 @@
+from rocolib.api.components.Component import Component
+
+
+c = Component()
+c.addParameter("width", 60, paramType="length", minValue=60)
+c.addParameter("length", 80, paramType="length", minValue=77)
+c.addParameter("height", 25, paramType="length", minValue=20)
+
+c.addSubcomponent("paperbot", "ESPSeg", inherit="length width height battery".split(), prefix=None)
+
+c.toLibrary("Paperbot")
diff --git a/rocolib/builders/RockerChairBuilder.py b/rocolib/builders/RockerChairBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..14739ed9a3b119628e6162909261febec5a8aa69
--- /dev/null
+++ b/rocolib/builders/RockerChairBuilder.py
@@ -0,0 +1,24 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("seat","ChairSeat", inherit=True, prefix=None, root=True)
+c.addSubcomponent("legl","RockerLeg", inherit=True, prefix=None)
+c.addSubcomponent("legr","RockerLeg", inherit=True, prefix=None)
+c.addSubcomponent("crossbar","Rectangle")
+
+c.delParameter("flip")
+
+c.addConstConstraint(("legl","flip"), True)
+c.addConstConstraint(("legr","flip"), False)
+
+c.addConstraint(("crossbar","l"), "width")
+c.addConstraint(("crossbar","w"), ("height", "rocker"), "x[0] * np.sin(np.deg2rad(x[1]))")
+
+c.addConnection(("seat","left"),("legl","topedge"), angle=0)
+c.addConnection(("seat","right"),("legr","topedge"), angle=0)
+
+c.addConnection(("crossbar","l"),("legl","crossbarflip"), angle=90)
+c.addConnection(("crossbar","r"),("legr","crossbar"), angle=90)
+
+c.toLibrary("RockerChair")
diff --git a/rocolib/builders/RockerLegBuilder.py b/rocolib/builders/RockerLegBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f80fa4e17af13476469b1699e4782969c2392e0
--- /dev/null
+++ b/rocolib/builders/RockerLegBuilder.py
@@ -0,0 +1,60 @@
+from numbers import Number
+
+from rocolib.api.components.Component import Component
+
+
+c = Component()
+
+c.addParameter("height", 40, paramType="length")
+c.addParameter("depth", 50, paramType="length")
+c.addParameter("thickness", 10, paramType="length")
+c.addParameter("rocker", 10, paramType="angle")
+c.addParameter("flip", False, valueType="bool")
+
+l = [
+    ["depth"], 
+    (("height", "rocker", "flip"), "1 * x[0] * np.sin(np.deg2rad(x[1] * x[2]))"), 
+    (("height", "rocker", "flip"), "1 * x[0] * np.sin(np.deg2rad(x[1] * x[2]))"), 
+    ["height"], 
+    ["depth"], 
+    ["height"], 
+    (("height", "rocker", "flip"), "1 * x[0] * np.sin(np.deg2rad(x[1] * (1-x[2])))"),
+    (("height", "rocker", "flip"), "1 * x[0] * np.sin(np.deg2rad(x[1] * (1-x[2])))"),
+    ]
+
+a = [ 
+    [["rocker", "flip"], "x[0] * x[1]"], 
+    0,
+    (["rocker", "flip"], "90+(x[0] * x[1])"), 
+    [["rocker", "flip"], "90-(x[0]*2 * x[1])"], 
+    [["rocker", "flip"], "90-(x[0]*2 * (1-x[1]))"], 
+    (["rocker", "flip"], "90+(x[0] * (1-x[1]))"), 
+    0,
+    [["rocker", "flip"], "x[0] * (1 - x[1])"],
+    ]
+
+n = len(l)
+
+for i in range(n):
+    c.addSubcomponent("beam%d" % i, "Rectangle")
+    c.addSubcomponent("kite%d" % i, "Kite", inherit="thickness", prefix=None)
+
+    c.addConstraint(("beam%d" % i, "w"), *l[i])
+    c.addConstraint(("beam%d" % i, "l"), "thickness")
+    if isinstance(a[i], Number):
+        c.addConstConstraint(("kite%d" % i, "angle"), a[i])
+    else:
+        c.addConstraint(("kite%d" % i, "angle"), *a[i])
+
+    c.addConnection(("beam%d" % i,"t"),("kite%d" % i,"b"), angle=0)
+    if i:
+        c.addConnection(("beam%d" % i,"b"),("kite%d" % ((i - 1) % n),"t"), angle=0)
+
+c.addConnection(("beam0","b"),("kite%d" % (n - 1),"t"), angle=0)
+
+
+c.inheritInterface("topedge", ("beam4", "r"))
+c.inheritInterface("crossbar", ("beam7", "l"))
+c.inheritInterface("crossbarflip", ("beam1", "l"))
+
+c.toLibrary("RockerLeg")
diff --git a/rocolib/builders/ServoMountBuilder.py b/rocolib/builders/ServoMountBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f7098e9f4768d0996f9a4df51efe5384a8cc7da
--- /dev/null
+++ b/rocolib/builders/ServoMountBuilder.py
@@ -0,0 +1,35 @@
+from rocolib.api.components.Component import Component
+from rocolib.api.Function import Function
+
+c = Component()
+
+c.addParameter("servo", "fs90r", paramType="dimension")
+c.addParameter("flip", False, valueType="bool")
+c.addParameter("center", True, valueType="bool")
+c.addParameter("shift", 0, paramType="length")
+c.addParameter("offset", optional=True)
+
+c.addParameter("length", 10, paramType="length")
+c.addParameter("width", 10, paramType="length")
+c.addParameter("depth", 10, paramType="length")
+
+c.addSubcomponent("beam", "RectBeam", inherit=True, prefix=None)
+c.addSubcomponent("mount", "Cutout")
+
+c.addConstraint(("beam", "width"), "depth")
+c.addConstraint(("beam", "depth"), "width")
+
+c.addConstraint(("beam", "minlength"), "servo", 'getDim(x, "motorlength") + getDim(x ,"shoulderlength") * 2')
+c.addConstraint(("beam", "minwidth"), "servo", 'getDim(x, "motorwidth")')
+c.addConstraint(("beam", "mindepth"), "servo", 'getDim(x, "motorheight")')
+
+c.addConstraint(("mount", "dx"), "servo", 'getDim(x, "motorwidth") * 0.99')
+c.addConstraint(("mount", "dy"), "servo", 'getDim(x, "motorlength")')
+
+c.inheritAllInterfaces("beam", prefix=None)
+c.inheritAllInterfaces("mount")
+c.addConnection(("beam", "face2"),
+                ("mount", "decoration"),
+                mode="hole", offset=Function(params="offset"))
+
+c.toLibrary("ServoMount")
diff --git a/rocolib/builders/SimpleChairBuilder.py b/rocolib/builders/SimpleChairBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3bf8b3d6c61a80460cea35878eb367e00a0d814
--- /dev/null
+++ b/rocolib/builders/SimpleChairBuilder.py
@@ -0,0 +1,15 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("seat","ChairSeat", inherit=True, prefix=None, root=True)
+c.addSubcomponent("legl","VLeg", inherit=True, prefix=None)
+c.addSubcomponent("legr","VLeg", inherit=True, prefix=None)
+
+c.addConstraint(("legl","width"), "depth")
+c.addConstraint(("legr","width"), "depth")
+
+c.addConnection(("seat","left"),("legl","topedge"), angle=0)
+c.addConnection(("seat","right"),("legr","topedge"), angle=0)
+
+c.toLibrary("SimpleChair")
diff --git a/rocolib/builders/SimpleTableBuilder.py b/rocolib/builders/SimpleTableBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..609349d92029eac876cd660978fc10f94dcf3104
--- /dev/null
+++ b/rocolib/builders/SimpleTableBuilder.py
@@ -0,0 +1,30 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+c.addSubcomponent("top","Rectangle", root=True)
+c.addSubcomponent("legl","VLeg", inherit=True, prefix=None)
+c.addSubcomponent("legr","VLeg", inherit=True, prefix=None)
+c.addSubcomponent("legt","VLeg", inherit=True, prefix=None)
+c.addSubcomponent("legb","VLeg", inherit=True, prefix=None)
+
+c.addParameter("length", 70, paramType="length")
+
+c.addConstraint(("top","l"), "length")
+c.addConstraint(("top","w"), "width")
+
+c.addConstraint(("legt","width"), "length")
+c.addConstraint(("legb","width"), "length")
+
+c.addConnection(("top","l"),("legl","topedge"), angle=90)
+c.addConnection(("top","r"),("legr","topedge"), angle=90)
+c.addConnection(("top","t"),("legt","topedge"), angle=90)
+c.addConnection(("top","b"),("legb","topedge"), angle=90)
+
+c.addConnection(("legl","rightedge"),("legb","leftedge"), angle=90)
+c.addConnection(("legb","rightedge"),("legr","leftedge"), angle=90)
+c.addConnection(("legr","rightedge"),("legt","leftedge"), angle=90)
+#c.addConnection(("legt","rightedge"),("legl","leftedge"), angle=90)
+c.addConnection(("legl","leftedge"),("legt","rightedge"), angle=90)
+
+c.toLibrary("SimpleTable")
diff --git a/rocolib/builders/TrimaranBuilder.py b/rocolib/builders/TrimaranBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..16880d074cb743ce82be6b8c3c3c391e7e8340a1
--- /dev/null
+++ b/rocolib/builders/TrimaranBuilder.py
@@ -0,0 +1,36 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+# BOX
+
+c.addParameter("seats", 6, valueType="int", minValue=2, maxValue=10)
+c.addParameter("spacing", 25, paramType="length")
+
+for i in range(3):
+    c.addSubcomponent("boat%d"%i,"BoatBase", inherit=True, prefix=None, root=True)
+
+    c.addSubcomponent("portsplit%d"%i,"SplitEdge")
+    c.addSubcomponent("starsplit%d"%i,"SplitEdge")
+
+    c.addConstraint(("portsplit%d"%i,"botlength"), ("boat.length", "seats"), "[x[0]]")
+    c.addConstraint(("portsplit%d"%i,"toplength"), ("boat.length", "seats"), "[x[0]/(1.*x[1])] * x[1]")
+    c.addConstraint(("starsplit%d"%i,"toplength"), ("boat.length", "seats"), "[x[0]]")
+    c.addConstraint(("starsplit%d"%i,"botlength"), ("boat.length", "seats"), "[x[0]/(1.*x[1])] * x[1]")
+
+    c.addConnection(("portsplit%d"%i, "botedge0"), ("boat%d"%i, "portedge"), angle=-90)
+    c.addConnection(("starsplit%d"%i, "topedge0"), ("boat%d"%i, "staredge"), angle=-90)
+
+for i in range(10):
+    nm = "seat%d"%i
+    c.addSubcomponent(nm, "Rectangle")
+    c.addConstraint((nm, "l"), ("spacing", "seats"), "(%d < x[1]) and x[0] or 0" % i)
+    c.addConstraint((nm, "w"), ("boat.length", "seats"), "x[0]/(1.*x[1])")
+    if (i % 2):
+        c.addConnection(("starsplit0", "botedge%d" % i), (nm, "l"))
+        c.addConnection(("portsplit1", "topedge%d" % i), (nm, "r"))
+    else:
+        c.addConnection(("starsplit1", "botedge%d" % i), (nm, "l"))
+        c.addConnection(("portsplit2", "topedge%d" % i), (nm, "r"))
+
+c.toLibrary("Trimaran")
diff --git a/rocolib/builders/TugBuilder.py b/rocolib/builders/TugBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..1fde8a2f8d36f5c4fa6b8d8d0fdc7b2cd7170923
--- /dev/null
+++ b/rocolib/builders/TugBuilder.py
@@ -0,0 +1,19 @@
+from rocolib.api.components.Component import Component
+
+c = Component()
+
+# BOX
+
+c.addSubcomponent("cabin","Cabin", inherit=True, prefix=None)
+c.addSubcomponent("boat","BoatBase", root=True)
+
+c.addConstraint(("boat","boat.length"), ("length", "depth"), "sum(x)")
+c.addConstraint(("boat","boat.width"), "width")
+c.addConstraint(("boat","boat.depth"), "width", "x/3.")
+c.addConstraint(("boat","bow.point"), "length", "x/2.")
+c.addConstraint(("boat","stern.point"), "length", "x/8.")
+
+c.addConnection(("cabin", "portedge"), ("boat", "portedge"), angle=0)
+c.addConnection(("cabin", "staredge"), ("boat", "staredge"), angle=0, tabWidth=10)
+
+c.toLibrary("Tug")
diff --git a/rocolib/builders/WheelBuilder.py b/rocolib/builders/WheelBuilder.py
new file mode 100644
index 0000000000000000000000000000000000000000..183a5cf90d74330ea705a914decc7158a59f79b1
--- /dev/null
+++ b/rocolib/builders/WheelBuilder.py
@@ -0,0 +1,15 @@
+from rocolib.api.components.Component import Component
+from rocolib.api.Function import Function
+
+c = Component()
+
+c.addSubcomponent("drive", "MountedServo", inherit=True, prefix=None)
+c.addSubcomponent("tire", "RegularNGon", inherit="radius", prefix=None)
+
+c.addConstConstraint(("tire", "n"), 40)
+
+c.inheritAllInterfaces("drive", prefix=None)
+c.addConnection(("drive", "mount"),
+                ("tire", "face"))
+
+c.toLibrary("Wheel")
diff --git a/rocolib/library/BoatBase.yaml b/rocolib/library/BoatBase.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3c13407c663fd0b04cfaf87d227ba0ce78c124b5
--- /dev/null
+++ b/rocolib/library/BoatBase.yaml
@@ -0,0 +1,255 @@
+connections:
+  connection0:
+  - - boat
+    - top
+  - - bow
+    - edge
+  - {}
+  connection1:
+  - - boat
+    - bot
+  - - stern
+    - edge
+  - {}
+interfaces:
+  portedge:
+    interface: ledge
+    subcomponent: boat
+  staredge:
+    interface: redge
+    subcomponent: boat
+parameters:
+  boat._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat.depth:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  boat.length:
+    defaultValue: 100
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  boat.width:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  bow._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow.point:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  stern._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern.point:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/BoatBaseBuilder.py
+subcomponents:
+  boat:
+    classname: SimpleUChannel
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: boat._dx
+      _dy:
+        parameter: boat._dy
+      _dz:
+        parameter: boat._dz
+      _q_a:
+        parameter: boat._q_a
+      _q_i:
+        parameter: boat._q_i
+      _q_j:
+        parameter: boat._q_j
+      _q_k:
+        parameter: boat._q_k
+      depth:
+        parameter: boat.depth
+      length:
+        parameter: boat.length
+      width:
+        parameter: boat.width
+  bow:
+    classname: BoatPoint
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: bow._dx
+      _dy:
+        parameter: bow._dy
+      _dz:
+        parameter: bow._dz
+      _q_a:
+        parameter: bow._q_a
+      _q_i:
+        parameter: bow._q_i
+      _q_j:
+        parameter: bow._q_j
+      _q_k:
+        parameter: bow._q_k
+      depth:
+        parameter: depth
+        subcomponent: boat
+      point:
+        parameter: bow.point
+      width:
+        parameter: width
+        subcomponent: boat
+  stern:
+    classname: BoatPoint
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: stern._dx
+      _dy:
+        parameter: stern._dy
+      _dz:
+        parameter: stern._dz
+      _q_a:
+        parameter: stern._q_a
+      _q_i:
+        parameter: stern._q_i
+      _q_j:
+        parameter: stern._q_j
+      _q_k:
+        parameter: stern._q_k
+      depth:
+        parameter: depth
+        subcomponent: boat
+      point:
+        parameter: stern.point
+      width:
+        parameter: width
+        subcomponent: boat
diff --git a/rocolib/library/BoatPoint.py b/rocolib/library/BoatPoint.py
new file mode 100644
index 0000000000000000000000000000000000000000..70872b2812ad72cb0c3ea2403f020697250ae696
--- /dev/null
+++ b/rocolib/library/BoatPoint.py
@@ -0,0 +1,82 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle, RightTriangle
+import rocolib.utils.numsym as math
+
+
+def pyramid(l,w,h):
+    def baseangle(x, y):
+        x2 = x/2.
+        hx = math.sqrt(h*h+x2*x2)
+        xa = math.rad2deg(math.arctan2(h,x2))
+        ba = math.rad2deg(math.arctan2(hx,y/2.))
+        return x2, hx, xa, ba
+
+    l2, hl, la, bla = baseangle(l, w)
+    w2, hw, wa, bwa = baseangle(w, l)
+    diag = math.sqrt(h*h + w2*w2 + l2+l2)
+    da = math.rad2deg(math.arccos(1/math.sqrt((1 + h*h/w2/w2)*(1+h*h/l2/l2))))
+    return hl, la, bla, hw, wa, bwa, diag, da
+
+
+class BoatPoint(FoldedComponent):
+    def define(self):
+        self.addParameter("width", 50, paramType="length")
+        self.addParameter("depth", 25, paramType="length")
+        self.addParameter("point", 50, paramType="length", minValue=0)
+
+        self.addEdgeInterface("ledge", "sl.e1", "depth")
+        self.addEdgeInterface("cedge", "sc.e2", "width")
+        self.addEdgeInterface("redge", "sr.e1", "depth")
+
+        self.addEdgeInterface("edge", ["sl.e1", "sc.e2", "sr.e1"], ["depth", "width", "depth"])
+
+    def assemble(self):
+        w = self.getParameter("width")
+        d = self.getParameter("depth")
+        p = self.getParameter("point")
+
+        hl, la, bla, hw, wa, bwa, diag, da = pyramid(d*2,w,p)
+
+        # Flaps
+        ba = (180 - bla - bwa)/2.
+        fx = d * math.tan(math.deg2rad(ba))
+
+        fll = Face("", ((fx, 0), (hw, 0), (0, d), (0,0)))
+        flc = RightTriangle("", w/2., hl);
+        frc = RightTriangle("", hl, w/2.);
+        frr = Face("", ((d, 0), (0, hw), (0, fx), (0,0)))
+
+        # Main point
+        self.addFace(fll, "lt");
+        self.attachEdge("lt.e2", flc, "e1", prefix="lc", angle=da)
+        self.attachEdge("lc.e2", frc, "e0", prefix="rc", angle=0)
+        self.attachEdge("rc.e1", frr, "e1", prefix="rt", angle=da)
+
+        # Flaps
+        fla = RightTriangle("", d, fx);
+        flb = RightTriangle("", fx, d);
+        self.attachEdge("lt.e3", fla, "e0", prefix="fla", angle=180)
+        self.attachEdge("fla.e1", flb, "e1", prefix="flb", angle=-180)
+
+        fra = RightTriangle("", fx, d);
+        frb = RightTriangle("", d, fx);
+        self.attachEdge("rt.e0", fra, "e2", prefix="fra", angle=180)
+        self.attachEdge("fra.e1", frb, "e1", prefix="frb", angle=-180)
+
+        self.addTab("flb.e0", "lt.e0", angle = -174, width=fx/3)
+        self.addTab("frb.e2", "rt.e3", angle = -174, width=fx/3)
+
+        # To allow 0 degree attachments at base
+        se = Face("", ((0,0), (w/2., 0), (-w/2., 0)))
+        self.attachEdge("lc.e0", se, "e0", prefix="sc", angle=90-la)
+        self.mergeEdge("rc.e2", "sc.e1", angle=90-la)
+
+        se = Face("", ((0,0), (d,0)))
+        self.attachEdge("flb.e2", se, "e0", prefix="sl", angle=90-wa)
+
+        se = Face("", ((0,0), (d,0)))
+        self.attachEdge("frb.e0", se, "e0", prefix="sr", angle=90-wa)
+
+if __name__ == "__main__":
+    BoatPoint.test()
+
diff --git a/rocolib/library/Cabin.yaml b/rocolib/library/Cabin.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..48fb07675b68338bbde94ccf4ffa2544da985275
--- /dev/null
+++ b/rocolib/library/Cabin.yaml
@@ -0,0 +1,167 @@
+connections:
+  connection0:
+  - - top
+    - b
+  - - rear
+    - t
+  - angle: 90
+  connection1:
+  - - top
+    - t
+  - - fore
+    - b
+  - angle: 90
+  connection2:
+  - - top
+    - l
+  - - port
+    - r
+  - angle: 90
+  connection3:
+  - - top
+    - r
+  - - star
+    - l
+  - angle: 90
+  connection4:
+  - - port
+    - t
+  - - fore
+    - l
+  - angle: 90
+    tabWidth: 10
+  connection5:
+  - - fore
+    - r
+  - - star
+    - t
+  - angle: 90
+    tabWidth: 10
+  connection6:
+  - - star
+    - b
+  - - rear
+    - r
+  - angle: 90
+    tabWidth: 10
+  connection7:
+  - - port
+    - b
+  - - rear
+    - l
+  - angle: 90
+    tabWidth: 10
+  connection8:
+  - - portsplit
+    - topedge1
+  - - port
+    - l
+  - {}
+  connection9:
+  - - starsplit
+    - topedge1
+  - - star
+    - r
+  - {}
+interfaces:
+  foreedge:
+    interface: t
+    subcomponent: fore
+  portedge:
+    interface: botedge0
+    subcomponent: portsplit
+  rearedge:
+    interface: b
+    subcomponent: rear
+  staredge:
+    interface: botedge0
+    subcomponent: starsplit
+parameters:
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 30
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 200
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 60
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/CabinBuilder.py
+subcomponents:
+  fore:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: width
+      w:
+        parameter: height
+  port:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: height
+      w:
+        parameter: depth
+  portsplit:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[sum(x)]'
+        parameter: &id001
+        - length
+        - depth
+      toplength:
+        function: '[x[0]/2., x[1], x[0]/2.]'
+        parameter: *id001
+  rear:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: width
+      w:
+        parameter: height
+  star:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: height
+      w:
+        parameter: depth
+  starsplit:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[sum(x)]'
+        parameter: *id001
+      toplength:
+        function: '[x[0]/2., x[1], x[0]/2.]'
+        parameter: *id001
+  top:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: width
+      w:
+        parameter: depth
diff --git a/rocolib/library/Canoe.yaml b/rocolib/library/Canoe.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ee32d94a740f973ca132f67250b4cb01bc5f605e
--- /dev/null
+++ b/rocolib/library/Canoe.yaml
@@ -0,0 +1,481 @@
+connections:
+  connection0:
+  - - portsplit
+    - botedge0
+  - - boat
+    - portedge
+  - angle: 90
+  connection1:
+  - - starsplit
+    - topedge0
+  - - boat
+    - staredge
+  - angle: 90
+    tabWidth: 10
+  connection10:
+  - - portsplit
+    - topedge9
+  - - seat4
+    - l
+  - {}
+  connection11:
+  - - starsplit
+    - botedge9
+  - - seat4
+    - r
+  - {}
+  connection12:
+  - - portsplit
+    - topedge11
+  - - seat5
+    - l
+  - {}
+  connection13:
+  - - starsplit
+    - botedge11
+  - - seat5
+    - r
+  - {}
+  connection14:
+  - - portsplit
+    - topedge13
+  - - seat6
+    - l
+  - {}
+  connection15:
+  - - starsplit
+    - botedge13
+  - - seat6
+    - r
+  - {}
+  connection16:
+  - - portsplit
+    - topedge15
+  - - seat7
+    - l
+  - {}
+  connection17:
+  - - starsplit
+    - botedge15
+  - - seat7
+    - r
+  - {}
+  connection18:
+  - - portsplit
+    - topedge17
+  - - seat8
+    - l
+  - {}
+  connection19:
+  - - starsplit
+    - botedge17
+  - - seat8
+    - r
+  - {}
+  connection2:
+  - - portsplit
+    - topedge1
+  - - seat0
+    - l
+  - {}
+  connection20:
+  - - portsplit
+    - topedge19
+  - - seat9
+    - l
+  - {}
+  connection21:
+  - - starsplit
+    - botedge19
+  - - seat9
+    - r
+  - {}
+  connection3:
+  - - starsplit
+    - botedge1
+  - - seat0
+    - r
+  - {}
+  connection4:
+  - - portsplit
+    - topedge3
+  - - seat1
+    - l
+  - {}
+  connection5:
+  - - starsplit
+    - botedge3
+  - - seat1
+    - r
+  - {}
+  connection6:
+  - - portsplit
+    - topedge5
+  - - seat2
+    - l
+  - {}
+  connection7:
+  - - starsplit
+    - botedge5
+  - - seat2
+    - r
+  - {}
+  connection8:
+  - - portsplit
+    - topedge7
+  - - seat3
+    - l
+  - {}
+  connection9:
+  - - starsplit
+    - botedge7
+  - - seat3
+    - r
+  - {}
+interfaces: {}
+parameters:
+  boat._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat.depth:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  boat.length:
+    defaultValue: 100
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  boat.width:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  bow._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow.point:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  seats:
+    defaultValue: 3
+    spec:
+      maxValue: 10
+      minValue: 1
+      valueType: int
+  stern._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern.point:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/CanoeBuilder.py
+subcomponents:
+  boat:
+    classname: BoatBase
+    kwargs:
+      root: true
+    parameters:
+      boat._dx:
+        parameter: boat._dx
+      boat._dy:
+        parameter: boat._dy
+      boat._dz:
+        parameter: boat._dz
+      boat._q_a:
+        parameter: boat._q_a
+      boat._q_i:
+        parameter: boat._q_i
+      boat._q_j:
+        parameter: boat._q_j
+      boat._q_k:
+        parameter: boat._q_k
+      boat.depth:
+        parameter: boat.depth
+      boat.length:
+        parameter: boat.length
+      boat.width:
+        parameter: boat.width
+      bow._dx:
+        parameter: bow._dx
+      bow._dy:
+        parameter: bow._dy
+      bow._dz:
+        parameter: bow._dz
+      bow._q_a:
+        parameter: bow._q_a
+      bow._q_i:
+        parameter: bow._q_i
+      bow._q_j:
+        parameter: bow._q_j
+      bow._q_k:
+        parameter: bow._q_k
+      bow.point:
+        parameter: bow.point
+      stern._dx:
+        parameter: stern._dx
+      stern._dy:
+        parameter: stern._dy
+      stern._dz:
+        parameter: stern._dz
+      stern._q_a:
+        parameter: stern._q_a
+      stern._q_i:
+        parameter: stern._q_i
+      stern._q_j:
+        parameter: stern._q_j
+      stern._q_k:
+        parameter: stern._q_k
+      stern.point:
+        parameter: stern.point
+  portsplit:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: (x[0],)
+        parameter: &id001
+        - boat.length
+        - seats
+      toplength:
+        function: (x[0]/(2.*x[1]+1.),) * (2*x[1]+1)
+        parameter: *id001
+  seat0:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (0 < x[1]) and x[0] or 0
+        parameter: &id002
+        - boat.width
+        - seats
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat1:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (1 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat2:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (2 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat3:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (3 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat4:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (4 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat5:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (5 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat6:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (6 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat7:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (7 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat8:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (8 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  seat9:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (9 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(2.*x[1]+1.)
+        parameter: *id001
+  starsplit:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: (x[0]/(2.*x[1]+1.),) * (2*x[1]+1)
+        parameter: *id001
+      toplength:
+        function: (x[0],)
+        parameter: *id001
diff --git a/rocolib/library/CatFoil.yaml b/rocolib/library/CatFoil.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b52bb88276bcbdc667b7c942d1edf08aea75f296
--- /dev/null
+++ b/rocolib/library/CatFoil.yaml
@@ -0,0 +1,102 @@
+connections:
+  connection0:
+  - - boat
+    - portedge
+  - - port
+    - mount
+  - angle: -180
+  connection1:
+  - - boat
+    - staredge
+  - - star
+    - mount
+  - angle: -180
+  connection2:
+  - - star
+    - join
+  - - port
+    - join
+  - tabWidth: 10
+interfaces: {}
+parameters:
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  dl:
+    defaultValue: 0.1
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 30
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 200
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 60
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/CatFoilBuilder.py
+subcomponents:
+  boat:
+    classname: Catamaran
+    kwargs:
+      root: true
+    parameters:
+      depth:
+        parameter: depth
+      height:
+        parameter: height
+      length:
+        parameter: length
+      width:
+        parameter: width
+  port:
+    classname: Foil
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: depth
+      dl:
+        parameter: dl
+      flip: true
+      height:
+        function: sum(x)/3.
+        parameter: &id001
+        - length
+        - depth
+      length:
+        parameter: length
+      width:
+        function: x/4.
+        parameter: width
+  star:
+    classname: Foil
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: depth
+      dl:
+        parameter: dl
+      flip: false
+      height:
+        function: sum(x)/3.
+        parameter: *id001
+      length:
+        parameter: length
+      width:
+        function: x/4.
+        parameter: width
diff --git a/rocolib/library/Catamaran.yaml b/rocolib/library/Catamaran.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1de83b1b6952d00acb67af27651ce6cae89ec3b5
--- /dev/null
+++ b/rocolib/library/Catamaran.yaml
@@ -0,0 +1,106 @@
+connections:
+  connection0:
+  - - cabin
+    - portedge
+  - - port
+    - portedge
+  - {}
+  connection1:
+  - - cabin
+    - staredge
+  - - star
+    - staredge
+  - {}
+interfaces:
+  foreedge:
+    interface: foreedge
+    subcomponent: cabin
+  portedge:
+    interface: staredge
+    subcomponent: port
+  rearedge:
+    interface: rearedge
+    subcomponent: cabin
+  staredge:
+    interface: portedge
+    subcomponent: star
+parameters:
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 30
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 200
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 60
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/CatBuilder.py
+subcomponents:
+  cabin:
+    classname: Cabin
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: depth
+      height:
+        parameter: height
+      length:
+        parameter: length
+      width:
+        parameter: width
+  port:
+    classname: BoatBase
+    kwargs:
+      root: true
+    parameters:
+      boat.depth:
+        function: sum(x)/20.
+        parameter: &id001
+        - length
+        - depth
+      boat.length:
+        function: sum(x)
+        parameter: *id001
+      boat.width:
+        function: x/4.
+        parameter: width
+      bow.point:
+        function: x/2.
+        parameter: length
+      stern.point:
+        function: x/8.
+        parameter: length
+  star:
+    classname: BoatBase
+    kwargs: {}
+    parameters:
+      boat.depth:
+        function: sum(x)/20.
+        parameter: *id001
+      boat.length:
+        function: sum(x)
+        parameter: *id001
+      boat.width:
+        function: x/4.
+        parameter: width
+      bow.point:
+        function: x/2.
+        parameter: length
+      stern.point:
+        function: x/8.
+        parameter: length
diff --git a/rocolib/library/ChairBack.yaml b/rocolib/library/ChairBack.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b14efdb46c51e598fe2518a4614591cbaa8b35da
--- /dev/null
+++ b/rocolib/library/ChairBack.yaml
@@ -0,0 +1,73 @@
+connections:
+  connection0:
+  - - panel
+    - tr
+  - - sider
+    - t
+  - angle: 0
+  connection1:
+  - - panel
+    - tl
+  - - sidel
+    - t
+  - angle: 0
+interfaces:
+  left:
+    interface: b
+    subcomponent: sidel
+  right:
+    interface: b
+    subcomponent: sider
+parameters:
+  backheight:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  gapheight:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 70
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/ChairBackBuilder.py
+subcomponents:
+  panel:
+    classname: ChairPanel
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: backheight
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
+  sidel:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: gapheight
+  sider:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: gapheight
diff --git a/rocolib/library/ChairPanel.yaml b/rocolib/library/ChairPanel.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..58fb8d8a3bcf094aaf758eb024cae05f14afe9af
--- /dev/null
+++ b/rocolib/library/ChairPanel.yaml
@@ -0,0 +1,78 @@
+connections:
+  connection0:
+  - - sidel
+    - r
+  - - back
+    - l
+  - angle: 90
+  connection1:
+  - - back
+    - r
+  - - sider
+    - l
+  - angle: 90
+interfaces:
+  bl:
+    interface: b
+    subcomponent: sidel
+  br:
+    interface: b
+    subcomponent: sider
+  left:
+    interface: l
+    subcomponent: sidel
+  right:
+    interface: r
+    subcomponent: sider
+  tl:
+    interface: t
+    subcomponent: sidel
+  tr:
+    interface: t
+    subcomponent: sider
+parameters:
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 70
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/ChairPanelBuilder.py
+subcomponents:
+  back:
+    classname: Rectangle
+    kwargs:
+      root: true
+    parameters:
+      l:
+        parameter: width
+      w:
+        parameter: depth
+  sidel:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: depth
+  sider:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: depth
diff --git a/rocolib/library/ChairSeat.yaml b/rocolib/library/ChairSeat.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..78bc361d75da67f4e4523d0ef760a3410e2f6193
--- /dev/null
+++ b/rocolib/library/ChairSeat.yaml
@@ -0,0 +1,113 @@
+connections:
+  connection0:
+  - - back
+    - left
+  - - kitel
+    - t
+  - angle: 0
+  connection1:
+  - - seat
+    - bl
+  - - kitel
+    - b
+  - angle: 0
+  connection2:
+  - - back
+    - right
+  - - kiter
+    - b
+  - angle: 0
+  connection3:
+  - - seat
+    - br
+  - - kiter
+    - t
+  - angle: 0
+interfaces:
+  left:
+    interface: left
+    subcomponent: seat
+  right:
+    interface: right
+    subcomponent: seat
+parameters:
+  backheight:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  gapheight:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  recline:
+    defaultValue: 110
+    spec:
+      maxValue: 360
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 70
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/ChairSeatBuilder.py
+subcomponents:
+  back:
+    classname: ChairBack
+    kwargs: {}
+    parameters:
+      backheight:
+        parameter: backheight
+      gapheight:
+        parameter: gapheight
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
+  kitel:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: 180 - x
+        parameter: recline
+      thickness:
+        parameter: thickness
+  kiter:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: 180 - x
+        parameter: recline
+      thickness:
+        parameter: thickness
+  seat:
+    classname: ChairPanel
+    kwargs:
+      root: true
+    parameters:
+      depth:
+        parameter: depth
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
diff --git a/rocolib/library/Cutout.py b/rocolib/library/Cutout.py
new file mode 100644
index 0000000000000000000000000000000000000000..77cb2bb855a3b682dadb19ff619373c8e67a60b9
--- /dev/null
+++ b/rocolib/library/Cutout.py
@@ -0,0 +1,21 @@
+from rocolib.api.components import DecorationComponent
+from rocolib.api.composables.graph.Face import Rectangle
+
+class Cutout(DecorationComponent):
+  def define(self):
+      self.addParameter("dx", 10, paramType="length")
+      self.addParameter("dy", 20, paramType="length")
+      self.addParameter("d", optional=True, overrides=("dx", "dy"))
+
+  def modifyParameters(self):
+    if self.getParameter("d") is not None:
+      self.setParameter("dx", self.getParameter("d"))
+      self.setParameter("dy", self.getParameter("d"))
+
+  def assemble(self):
+    dx = self.getParameter("dx")
+    dy = self.getParameter("dy")
+    self.addFace(Rectangle("r0", dx, dy), prefix="r0")
+
+if __name__ == "__main__":
+    Cutout.test()
diff --git a/rocolib/library/ESPBrains.yaml b/rocolib/library/ESPBrains.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..902c2bd4be821dbec9a41f038a19062f80099857
--- /dev/null
+++ b/rocolib/library/ESPBrains.yaml
@@ -0,0 +1,132 @@
+connections:
+  connection0:
+  - &id001
+    - beam
+    - face1
+  - - header
+    - decoration
+  - mode: hole
+    offset:
+      function: (7.5, -4.5 + 0.5*(x[0]-getDim(x[1], 'length')))
+      parameter: &id002
+      - length
+      - brain
+  connection1:
+  - *id001
+  - - servoPins
+    - decoration
+  - mode: hole
+    offset:
+      function: (-17.25, (0.5 * (x[0]-getDim(x[1], 'length')))-14)
+      parameter: *id002
+    rotate: true
+interfaces:
+  botedge0:
+    interface: botedge0
+    subcomponent: beam
+  botedge1:
+    interface: botedge1
+    subcomponent: beam
+  botedge2:
+    interface: botedge2
+    subcomponent: beam
+  botedge3:
+    interface: botedge3
+    subcomponent: beam
+  face0:
+    interface: face0
+    subcomponent: beam
+  face1:
+    interface: face1
+    subcomponent: beam
+  face2:
+    interface: face2
+    subcomponent: beam
+  face3:
+    interface: face3
+    subcomponent: beam
+  slotedge:
+    interface: slotedge
+    subcomponent: beam
+  tabedge:
+    interface: tabedge
+    subcomponent: beam
+  topedge0:
+    interface: topedge0
+    subcomponent: beam
+  topedge1:
+    interface: topedge1
+    subcomponent: beam
+  topedge2:
+    interface: topedge2
+    subcomponent: beam
+  topedge3:
+    interface: topedge3
+    subcomponent: beam
+parameters:
+  brain:
+    defaultValue: nodeMCU
+    spec:
+      valueType: str
+  depth:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 90
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/ESPBrainsBuilder.py
+subcomponents:
+  beam:
+    classname: RectBeam
+    kwargs: {}
+    parameters:
+      angle: 90
+      depth:
+        parameter: width
+      length:
+        parameter: length
+      mindepth:
+        function: getDim(x, 'width')
+        parameter: brain
+      minlength:
+        function: getDim(x, 'length')
+        parameter: brain
+      minwidth:
+        function: getDim(x, 'height')
+        parameter: brain
+      width:
+        parameter: depth
+  header:
+    classname: Header
+    kwargs: {}
+    parameters:
+      colsep:
+        function: getDim(x, 'colsep')
+        parameter: brain
+      ncols:
+        function: getDim(x, 'ncols')
+        parameter: brain
+      nrows:
+        function: getDim(x, 'nrows')
+        parameter: brain
+      rowsep:
+        function: getDim(x, 'rowsep')
+        parameter: brain
+  servoPins:
+    classname: Cutout
+    kwargs: {}
+    parameters:
+      dx: 27
+      dy: 8
diff --git a/rocolib/library/ESPSeg.py b/rocolib/library/ESPSeg.py
new file mode 100644
index 0000000000000000000000000000000000000000..44b8c2159c797953bdfe4825395bd875a082814f
--- /dev/null
+++ b/rocolib/library/ESPSeg.py
@@ -0,0 +1,15 @@
+from rocolib.api.components import Component
+from rocolib.utils.utils import copyDecorations
+
+class ESPSeg(Component):
+  def assemble(self):
+    copyDecorations(self, ("rightservoface", ("right", "face2", 1, 2)),
+                          ("rightservosheath", ("sheath", "face3", -1, 0)))
+    copyDecorations(self, ("leftservoface", ("left", "face2", 2, 1)),
+                          ("leftservosheath", ("sheath", "face1", 0, -1)))
+    copyDecorations(self, ("brainface", ("brain", "face1", 0, 1)),
+                          ("brainsheath", ("sheath", "face2", 1, 2)))
+
+if __name__ == "__main__":
+  ESPSeg.test()
+
diff --git a/rocolib/library/ESPSeg.yaml b/rocolib/library/ESPSeg.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6f73775b8a0aab24530bf87c3cca3c455e85cad0
--- /dev/null
+++ b/rocolib/library/ESPSeg.yaml
@@ -0,0 +1,233 @@
+connections:
+  connection0:
+  - - brain
+    - topedge0
+  - - right
+    - topedge2
+  - angle: -90
+  connection1:
+  - - brain
+    - botedge0
+  - - left
+    - topedge0
+  - angle: -90
+  connection2:
+  - - left
+    - botedge1
+  - - sheathsplit
+    - botedge2
+  - angle: 180
+  connection3:
+  - - sheathsplit
+    - topedge0
+  - - sheath
+    - topedge1
+  - {}
+  connection4:
+  - - tail
+    - topedge
+  - - sheath
+    - botedge1
+  - angle: 90
+  connection5:
+  - - sheath
+    - botedge3
+  - - tailsplit
+    - topedge0
+  - {}
+  connection6:
+  - - tail
+    - flapedge
+  - - tailsplit
+    - botedge1
+  - angle: 90
+    tabWidth:
+      function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))+x[2]
+      parameter:
+      - controller
+      - driveservo
+      - battery
+  connection7:
+  - - sheath
+    - face1
+  - - usb
+    - decoration
+  - mode: hole
+    offset:
+      function: (4-(max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))+x[3])/2,
+        0.5 * x[2] - 15)
+      parameter:
+      - controller
+      - driveservo
+      - length
+      - battery
+interfaces: {}
+parameters:
+  battery:
+    defaultValue: 7
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  controller:
+    defaultValue: nodeMCU
+    spec:
+      valueType: str
+  driveservo:
+    defaultValue: fs90r
+    spec:
+      valueType: str
+  flapwidth:
+    defaultValue: 0.2
+    spec:
+      maxValue: 1
+      minValue: 0
+      valueType: (float, int)
+  height:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 90
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  tailwidth:
+    defaultValue: 0.3333333333333333
+    spec:
+      maxValue: 1
+      minValue: 0
+      valueType: (float, int)
+  width:
+    defaultValue: 60
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/ESPSegBuilder.py
+subcomponents:
+  brain:
+    classname: ESPBrains
+    kwargs: {}
+    parameters:
+      brain:
+        parameter: controller
+      depth:
+        function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))
+        parameter:
+        - controller
+        - driveservo
+      length:
+        parameter: width
+  left:
+    classname: Wheel
+    kwargs:
+      invert: true
+    parameters:
+      angle: 90
+      center: false
+      depth:
+        function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))
+        parameter:
+        - controller
+        - driveservo
+      length:
+        function: x[0] - getDim(x[1],'width')
+        parameter: &id001
+        - length
+        - controller
+      phase: 2
+      radius:
+        parameter: height
+      servo:
+        parameter: driveservo
+  right:
+    classname: Wheel
+    kwargs:
+      invert: true
+    parameters:
+      angle: 90
+      center: false
+      depth:
+        function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))
+        parameter:
+        - controller
+        - driveservo
+      length:
+        function: x[0] - getDim(x[1],'width')
+        parameter: *id001
+      radius:
+        parameter: height
+      servo:
+        parameter: driveservo
+  sheath:
+    classname: RectBeam
+    kwargs: {}
+    parameters:
+      angle: 90
+      depth:
+        function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth')) + x[2]
+        parameter:
+        - controller
+        - driveservo
+        - battery
+      length:
+        parameter: length
+      phase: 1
+      width:
+        parameter: width
+  sheathsplit:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: (getDim(x[0],'motorheight'),           x[1] - 2*getDim(x[0],'motorheight'),           getDim(x[0],'motorheight'))
+        parameter:
+        - driveservo
+        - width
+      toplength:
+        function: (x,)
+        parameter: width
+  tail:
+    classname: Tail
+    kwargs: {}
+    parameters:
+      depth:
+        function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))+x[2]
+        parameter:
+        - controller
+        - driveservo
+        - battery
+      flapwidth:
+        parameter: flapwidth
+      height:
+        function: max(getDim(x[0],'height'), getDim(x[1],'motorwidth'))/2.+x[2]
+        parameter:
+        - controller
+        - driveservo
+        - height
+      tailwidth:
+        parameter: tailwidth
+      width:
+        parameter: width
+  tailsplit:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: (x[0]*(1-x[1])/2., x[0]*x[1], x[0]*(1-x[1])/2.)
+        parameter:
+        - width
+        - flapwidth
+      toplength:
+        function: (x,)
+        parameter: width
+  usb:
+    classname: Cutout
+    kwargs: {}
+    parameters:
+      dx: 4
+      dy: 9
diff --git a/rocolib/library/Foil.yaml b/rocolib/library/Foil.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..51f060224985c0d40572d27378913f0caf28ad58
--- /dev/null
+++ b/rocolib/library/Foil.yaml
@@ -0,0 +1,94 @@
+connections:
+  connection0:
+  - - split
+    - topedge1
+  - - stick
+    - l
+  - {}
+  connection1:
+  - - stick
+    - r
+  - - foil
+    - tip
+  - angle: 90
+interfaces:
+  join:
+    interface: base
+    subcomponent: foil
+  mount:
+    interface: botedge0
+    subcomponent: split
+parameters:
+  depth:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  dl:
+    defaultValue: 0.1
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  flip:
+    defaultValue: false
+    spec:
+      valueType: bool
+  height:
+    defaultValue: 100
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/FoilBuilder.py
+subcomponents:
+  foil:
+    classname: Wing
+    kwargs: {}
+    parameters:
+      bodylength:
+        parameter: depth
+      flip:
+        parameter: flip
+      thickness:
+        function: x[0] * x[1]
+        parameter:
+        - dl
+        - depth
+      wingspan:
+        parameter: width
+      wingtip:
+        parameter: depth
+  split:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[sum(x)]'
+        parameter: &id001
+        - length
+        - depth
+      toplength:
+        function: '[x[0]/2., x[1], x[0]/2.]'
+        parameter: *id001
+  stick:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: height
+      w:
+        parameter: depth
diff --git a/rocolib/library/Header.py b/rocolib/library/Header.py
new file mode 100644
index 0000000000000000000000000000000000000000..22827533dcc68707f52de3d36bb541fd5c2bb9f9
--- /dev/null
+++ b/rocolib/library/Header.py
@@ -0,0 +1,34 @@
+from rocolib.api.components import DecorationComponent
+from rocolib.api.composables.graph.Face import Face
+
+
+class Header(DecorationComponent):
+  def define(self):
+      self.addParameter("nrows", 3, paramType="count")
+      self.addParameter("ncols", 1, paramType="count")
+      self.addParameter("rowsep", 2.54, paramType="length")
+      self.addParameter("colsep", 2.54, paramType="length")
+      self.addParameter("diameter", 1, paramType="length")
+
+  def assemble(self):
+    diam = self.getParameter("diameter")/2.
+    nr = self.getParameter("nrows")
+    nc = self.getParameter("ncols")
+
+    def hole(i, j, d):
+      dx = (j - (nc-1)/2.)*self.getParameter("colsep")
+      dy = (i - (nr-1)/2.)*self.getParameter("rowsep")
+      return Face("r-%d-%d" % (i,j),
+                        ((dx-d, dy-d), (dx+d, dy-d), (dx+d, dy+d), (dx-d, dy+d)),
+                        recenter=False)
+
+    for i in range(nr):
+      for j in range(nc):
+        d = diam
+        if (i == 0 and j == 0) or \
+           (i == nr-1 and j == nc-1):
+               d = diam*3
+        self.addFace(hole(i,j,d), prefix="r-%d-%d" % (i,j))
+
+if __name__ == "__main__":
+    Header.test()
diff --git a/rocolib/library/Kite.py b/rocolib/library/Kite.py
new file mode 100644
index 0000000000000000000000000000000000000000..5796cc34d48baaff929a1e9dbd5ad25d8070069b
--- /dev/null
+++ b/rocolib/library/Kite.py
@@ -0,0 +1,25 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face
+from rocolib.utils.numsym import sin, cos, tan, deg2rad
+
+
+class Kite(FoldedComponent):
+  def define(self):
+    self.addParameter("thickness", 10, paramType="length")
+    self.addParameter("angle", 45, paramType="angle")
+
+    self.addEdgeInterface("b", "kite.e0", "thickness")
+    self.addEdgeInterface("t", "kite.e3", "thickness")
+
+  def assemble(self):
+    t = self.getParameter("thickness")
+    a = self.getParameter("angle")
+    a2 = deg2rad(a/2.)
+    ar = deg2rad(a)
+
+    s = Face("", ((t, 0), (t, t * tan(a2)), (t * cos(ar), t * sin(ar)), (0,0)))
+
+    self.addFace(s, "kite")
+
+if __name__ == "__main__":
+    Kite.test()
diff --git a/rocolib/library/MountedServo.yaml b/rocolib/library/MountedServo.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8db293b477ea12cd99d9cdaef40186477fbc5c10
--- /dev/null
+++ b/rocolib/library/MountedServo.yaml
@@ -0,0 +1,268 @@
+connections:
+  connection0:
+  - - mount
+    - mount.decoration
+  - - servo
+    - horn
+  - {}
+interfaces:
+  botedge0:
+    interface: botedge0
+    subcomponent: mount
+  botedge1:
+    interface: botedge1
+    subcomponent: mount
+  botedge2:
+    interface: botedge2
+    subcomponent: mount
+  botedge3:
+    interface: botedge3
+    subcomponent: mount
+  face0:
+    interface: face0
+    subcomponent: mount
+  face1:
+    interface: face1
+    subcomponent: mount
+  face2:
+    interface: face2
+    subcomponent: mount
+  face3:
+    interface: face3
+    subcomponent: mount
+  horn:
+    interface: horn
+    subcomponent: servo
+  mount:
+    interface: mount
+    subcomponent: servo
+  mount.decoration:
+    interface: mount.decoration
+    subcomponent: mount
+  slotedge:
+    interface: slotedge
+    subcomponent: mount
+  tabedge:
+    interface: tabedge
+    subcomponent: mount
+  topedge0:
+    interface: topedge0
+    subcomponent: mount
+  topedge1:
+    interface: topedge1
+    subcomponent: mount
+  topedge2:
+    interface: topedge2
+    subcomponent: mount
+  topedge3:
+    interface: topedge3
+    subcomponent: mount
+parameters:
+  _dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  addTabs:
+    defaultValue: true
+    spec:
+      valueType: bool
+  angle:
+    defaultValue: null
+    spec:
+      optional: true
+      overrides:
+      - tangle
+      - bangle
+  bangle:
+    defaultValue: 135
+    spec:
+      maxValue: 180
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  center:
+    defaultValue: true
+    spec:
+      valueType: bool
+  depth:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  flip:
+    defaultValue: false
+    spec:
+      valueType: bool
+  length:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  mindepth:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  minlength:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  minwidth:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  offset:
+    defaultValue: null
+    spec:
+      optional: true
+  phase:
+    defaultValue: 0
+    spec:
+      valueType: int
+  root:
+    defaultValue: null
+    spec:
+      optional: true
+      valueType: int
+  servo:
+    defaultValue: fs90r
+    spec:
+      valueType: str
+  shift:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  tangle:
+    defaultValue: 80
+    spec:
+      maxValue: 180
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  width:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/MountedServoBuilder.py
+subcomponents:
+  mount:
+    classname: ServoMount
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      addTabs:
+        parameter: addTabs
+      angle:
+        parameter: angle
+      bangle:
+        parameter: bangle
+      center:
+        parameter: center
+      depth:
+        parameter: depth
+      flip:
+        parameter: flip
+      length:
+        parameter: length
+      mindepth:
+        parameter: mindepth
+      minlength:
+        parameter: minlength
+      minwidth:
+        parameter: minwidth
+      offset:
+        parameter: offset
+      phase:
+        parameter: phase
+      root:
+        parameter: root
+      servo:
+        parameter: servo
+      shift:
+        parameter: shift
+      tangle:
+        parameter: tangle
+      width:
+        parameter: width
+  servo:
+    classname: ServoMotor
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      servo:
+        parameter: servo
diff --git a/rocolib/library/Paperbot.yaml b/rocolib/library/Paperbot.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..869fd0f99e59929f0ea25f270664f878295447b0
--- /dev/null
+++ b/rocolib/library/Paperbot.yaml
@@ -0,0 +1,41 @@
+connections: {}
+interfaces: {}
+parameters:
+  battery:
+    defaultValue: 7
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 25
+    spec:
+      minValue: 20
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 80
+    spec:
+      minValue: 77
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 60
+    spec:
+      minValue: 60
+      units: mm
+      valueType: (float, int)
+source: ../builders/PaperbotBuilder.py
+subcomponents:
+  paperbot:
+    classname: ESPSeg
+    kwargs: {}
+    parameters:
+      battery:
+        parameter: battery
+      height:
+        parameter: height
+      length:
+        parameter: length
+      width:
+        parameter: width
diff --git a/rocolib/library/RectBeam.py b/rocolib/library/RectBeam.py
new file mode 100644
index 0000000000000000000000000000000000000000..d45e0cd4075f1ed3e7085b1384831b9d8f3a6705
--- /dev/null
+++ b/rocolib/library/RectBeam.py
@@ -0,0 +1,90 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle
+import rocolib.utils.numsym as np
+
+class RectBeam(FoldedComponent):
+  def define(self):
+    self.addParameter("length", 100, paramType="length")
+    self.addParameter("width", 20, paramType="length")
+    self.addParameter("depth", 50, paramType="length")
+
+    # XXX TODO: incorporate into minValue of parameters somehow
+    self.addParameter("minlength", 0, paramType="length")
+    self.addParameter("minwidth", 0, paramType="length")
+    self.addParameter("mindepth", 0, paramType="length")
+
+    self.addParameter("phase", 0, valueType="int")
+
+    self.addParameter("angle", optional=True, overrides=("tangle", "bangle"))
+    self.addParameter("tangle", 80, paramType="angle", maxValue=180)
+    self.addParameter("bangle", 135, paramType="angle", maxValue=180)
+
+    self.addParameter("root", optional=True, valueType="int")
+    self.addParameter("addTabs", True, valueType="bool")
+
+    for i in range(4):
+      self.addEdgeInterface("topedge%d" % i, "r%d.e0" % i, ["width", "depth"][i % 2])
+      self.addEdgeInterface("botedge%d" % i, "r%d.e2" % i, ["width", "depth"][i % 2])
+      self.addFaceInterface("face%d" % i, "r%d" % i)
+    self.addEdgeInterface("tabedge", "r3.e1", "length")
+    self.addEdgeInterface("slotedge", "r0.e3", "length")
+
+  def modifyParameters(self):
+    self.setParameter("width", max(self.getParameter("width"), self.getParameter("minwidth")))
+    self.setParameter("depth", max(self.getParameter("depth"), self.getParameter("mindepth")))
+    self.setParameter("length", max(self.getParameter("length"), self.getParameter("minlength")))
+
+  def assemble(self):
+    if self.getParameter("angle") is not None:
+      bangle = 90 - self.getParameter("angle")
+      tangle = 90 - self.getParameter("angle")
+    else:
+      bangle = 90 - self.getParameter("bangle")
+      tangle = 90 - self.getParameter("tangle")
+
+    try:
+      root = self.getParameter("root")
+    except KeyError:
+      root = None
+
+    length = self.getParameter("length")
+    width = self.getParameter("width")
+    depth = self.getParameter("depth")
+    phase = self.getParameter("phase")
+
+    def dl(a):
+        return np.tan(np.deg2rad(a)) * depth
+
+    rs = []
+    rs.append(Rectangle("", width, length))
+    rs.append(Face("", (
+      (depth, dl(tangle)),
+      (depth, length - dl(bangle)),
+      (0, length), 
+      (0,0)
+    )))
+    rs.append(Rectangle("", width, length - dl(tangle) - dl(bangle)))
+    rs.append(Face("", (
+      (0, length), 
+      (0,0),
+      (depth, dl(bangle)),
+      (depth, length - dl(tangle))
+    )))
+
+    for i in range(phase):
+      rs.append(rs.pop(0))
+
+    fromEdge = None
+    for i in range(4):
+      self.attachEdge(fromEdge, rs[i], "e3", prefix="r%d"%i, angle=90, root=((i == root) if root is not None else False))
+      fromEdge = 'r%d.e1' % i
+      self.setFaceInterface("face%d" % i, "r%d" % ((i-phase)%4))
+
+    slotEdge = "r0.e3"
+    tabEdge = "r3.e1"
+
+    if self.getParameter("addTabs"):
+        self.addTab(slotEdge, tabEdge, angle= 90, width=min(10, [depth, width][phase % 2]))
+
+if __name__ == "__main__":
+  RectBeam.test()
diff --git a/rocolib/library/Rectangle.py b/rocolib/library/Rectangle.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab31221ac624f4f763c8063f4cc6df2ecd8e5cc3
--- /dev/null
+++ b/rocolib/library/Rectangle.py
@@ -0,0 +1,35 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Rectangle as Rect
+
+
+class Rectangle(FoldedComponent):
+
+  def define(self):
+    self.addParameter("l", 100, paramType="length")
+    self.addParameter("w", 400, paramType="length")
+
+    self.addEdgeInterface("b", "e0", "l")
+    self.addEdgeInterface("r", "e1", "w")
+    self.addEdgeInterface("t", "e2", "l")
+    self.addEdgeInterface("l", "e3", "w")
+    self.addFaceInterface("face", "r")
+
+  def assemble(self):
+    dx = self.getParameter("l")
+    dy = self.getParameter("w")
+
+    self.addFace(Rect("r", dx, dy))
+
+if __name__ == "__main__":
+    import sympy
+
+    Rectangle.test()
+
+    # Test sympy
+    r = Rectangle()
+    r.makeOutput(useDefaultParameters=False, default=False)
+    g = r.getGraph()
+    for f in g.faces:
+        sympy.pprint(f.transform3D)
+        sympy.pprint(f.get3DCoords())
+
diff --git a/rocolib/library/RegularNGon.py b/rocolib/library/RegularNGon.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1897fe05050e5a6692e07df6c2f73e0fb3fd740
--- /dev/null
+++ b/rocolib/library/RegularNGon.py
@@ -0,0 +1,26 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import RegularNGon2 as Shape
+
+
+class RegularNGon(FoldedComponent):
+  def define(self):
+    self.addParameter("n", 5, valueType="int", minValue=3)
+    self.addParameter("radius", 25, paramType="length")
+
+    self.addEdgeInterface("e0", "e0", "radius")
+    self.addFaceInterface("face", "r")
+
+  def assemble(self):
+    n = self.getParameter("n")
+    l = self.getParameter("radius")
+
+    self.addFace(Shape("r", n, l))
+
+    for i in range(n):
+        try: 
+            self.setEdgeInterface("e%d" % i, "e%d" % i, "radius")
+        except KeyError:
+            self.addEdgeInterface("e%d"%i, "e%d" % i, "radius")
+
+if __name__ == "__main__":
+    RegularNGon.test()
diff --git a/rocolib/library/RockerChair.yaml b/rocolib/library/RockerChair.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3793d6d02058bde27ea066351559c1f10679d574
--- /dev/null
+++ b/rocolib/library/RockerChair.yaml
@@ -0,0 +1,133 @@
+connections:
+  connection0:
+  - - seat
+    - left
+  - - legl
+    - topedge
+  - angle: 0
+  connection1:
+  - - seat
+    - right
+  - - legr
+    - topedge
+  - angle: 0
+  connection2:
+  - - crossbar
+    - l
+  - - legl
+    - crossbarflip
+  - angle: 90
+  connection3:
+  - - crossbar
+    - r
+  - - legr
+    - crossbar
+  - angle: 90
+interfaces: {}
+parameters:
+  backheight:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  gapheight:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  recline:
+    defaultValue: 110
+    spec:
+      maxValue: 360
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  rocker:
+    defaultValue: 10
+    spec:
+      maxValue: 360
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 70
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/RockerChairBuilder.py
+subcomponents:
+  crossbar:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: width
+      w:
+        function: x[0] * np.sin(np.deg2rad(x[1]))
+        parameter:
+        - height
+        - rocker
+  legl:
+    classname: RockerLeg
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: depth
+      flip: true
+      height:
+        parameter: height
+      rocker:
+        parameter: rocker
+      thickness:
+        parameter: thickness
+  legr:
+    classname: RockerLeg
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: depth
+      flip: false
+      height:
+        parameter: height
+      rocker:
+        parameter: rocker
+      thickness:
+        parameter: thickness
+  seat:
+    classname: ChairSeat
+    kwargs:
+      root: true
+    parameters:
+      backheight:
+        parameter: backheight
+      depth:
+        parameter: depth
+      gapheight:
+        parameter: gapheight
+      recline:
+        parameter: recline
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
diff --git a/rocolib/library/RockerLeg.yaml b/rocolib/library/RockerLeg.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e6b51ef15221587129c9b2624c136d1af403a019
--- /dev/null
+++ b/rocolib/library/RockerLeg.yaml
@@ -0,0 +1,290 @@
+connections:
+  connection0:
+  - - beam0
+    - t
+  - - kite0
+    - b
+  - angle: 0
+  connection1:
+  - - beam1
+    - t
+  - - kite1
+    - b
+  - angle: 0
+  connection10:
+  - - beam5
+    - b
+  - - kite4
+    - t
+  - angle: 0
+  connection11:
+  - - beam6
+    - t
+  - - kite6
+    - b
+  - angle: 0
+  connection12:
+  - - beam6
+    - b
+  - - kite5
+    - t
+  - angle: 0
+  connection13:
+  - - beam7
+    - t
+  - - kite7
+    - b
+  - angle: 0
+  connection14:
+  - - beam7
+    - b
+  - - kite6
+    - t
+  - angle: 0
+  connection15:
+  - - beam0
+    - b
+  - - kite7
+    - t
+  - angle: 0
+  connection2:
+  - - beam1
+    - b
+  - - kite0
+    - t
+  - angle: 0
+  connection3:
+  - - beam2
+    - t
+  - - kite2
+    - b
+  - angle: 0
+  connection4:
+  - - beam2
+    - b
+  - - kite1
+    - t
+  - angle: 0
+  connection5:
+  - - beam3
+    - t
+  - - kite3
+    - b
+  - angle: 0
+  connection6:
+  - - beam3
+    - b
+  - - kite2
+    - t
+  - angle: 0
+  connection7:
+  - - beam4
+    - t
+  - - kite4
+    - b
+  - angle: 0
+  connection8:
+  - - beam4
+    - b
+  - - kite3
+    - t
+  - angle: 0
+  connection9:
+  - - beam5
+    - t
+  - - kite5
+    - b
+  - angle: 0
+interfaces:
+  crossbar:
+    interface: l
+    subcomponent: beam7
+  crossbarflip:
+    interface: l
+    subcomponent: beam1
+  topedge:
+    interface: r
+    subcomponent: beam4
+parameters:
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  flip:
+    defaultValue: false
+    spec:
+      valueType: bool
+  height:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  rocker:
+    defaultValue: 10
+    spec:
+      maxValue: 360
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/RockerLegBuilder.py
+subcomponents:
+  beam0:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: depth
+  beam1:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        function: 1 * x[0] * np.sin(np.deg2rad(x[1] * x[2]))
+        parameter: &id001
+        - height
+        - rocker
+        - flip
+  beam2:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        function: 1 * x[0] * np.sin(np.deg2rad(x[1] * x[2]))
+        parameter: *id001
+  beam3:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: height
+  beam4:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: depth
+  beam5:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        parameter: height
+  beam6:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        function: 1 * x[0] * np.sin(np.deg2rad(x[1] * (1-x[2])))
+        parameter: *id001
+  beam7:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        parameter: thickness
+      w:
+        function: 1 * x[0] * np.sin(np.deg2rad(x[1] * (1-x[2])))
+        parameter: *id001
+  kite0:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: x[0] * x[1]
+        parameter:
+        - rocker
+        - flip
+      thickness:
+        parameter: thickness
+  kite1:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle: 0
+      thickness:
+        parameter: thickness
+  kite2:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: 90+(x[0] * x[1])
+        parameter:
+        - rocker
+        - flip
+      thickness:
+        parameter: thickness
+  kite3:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: 90-(x[0]*2 * x[1])
+        parameter:
+        - rocker
+        - flip
+      thickness:
+        parameter: thickness
+  kite4:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: 90-(x[0]*2 * (1-x[1]))
+        parameter:
+        - rocker
+        - flip
+      thickness:
+        parameter: thickness
+  kite5:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: 90+(x[0] * (1-x[1]))
+        parameter:
+        - rocker
+        - flip
+      thickness:
+        parameter: thickness
+  kite6:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle: 0
+      thickness:
+        parameter: thickness
+  kite7:
+    classname: Kite
+    kwargs: {}
+    parameters:
+      angle:
+        function: x[0] * (1 - x[1])
+        parameter:
+        - rocker
+        - flip
+      thickness:
+        parameter: thickness
diff --git a/rocolib/library/ServoMotor.py b/rocolib/library/ServoMotor.py
new file mode 100644
index 0000000000000000000000000000000000000000..589593730c2c0dfd0aefd05b2c9aefb1adc4a195
--- /dev/null
+++ b/rocolib/library/ServoMotor.py
@@ -0,0 +1,22 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Rectangle as Shape
+from rocolib.api.ports import AnchorPort
+from rocolib.utils.dimensions import getDim
+from rocolib.utils.transforms import Translate
+
+
+class ServoMotor(FoldedComponent):
+  def define(self):
+    self.addParameter("servo", "fs90r", paramType="dimension")
+    self.addInterface("horn", AnchorPort(self, self.getGraph(), "h", Translate([0,0,0])))
+    self.addFaceInterface("mount", "h")
+
+  def assemble(self):
+    s = self.getParameter("servo")
+    dz = getDim(s, "hornheight")
+
+    self.addFace(Shape("h", 0, 0))
+    self.setInterface("horn", AnchorPort(self, self.getGraph(), "h", Translate([0,0,dz])))
+
+if __name__ == "__main__":
+    ServoMotor.test()
diff --git a/rocolib/library/ServoMount.py b/rocolib/library/ServoMount.py
new file mode 100644
index 0000000000000000000000000000000000000000..628417f379c261365151c2d9b5360a32d0340224
--- /dev/null
+++ b/rocolib/library/ServoMount.py
@@ -0,0 +1,37 @@
+from rocolib.api.components.Component import Component
+from rocolib.api.ports.EdgePort import EdgePort
+from rocolib.utils.dimensions import getDim
+from rocolib.api.Function import Function
+
+
+class ServoMount(Component):
+  def modifyParameters(self):
+    servo = self.getParameter("servo")
+    minl_s = getDim(servo, "motorlength") + getDim(servo, "shoulderlength") * 2
+    minl_p = self.getParameter("minlength")
+    if minl_p is None:
+      minl = minl_s
+    else:
+      minl = min(minl_s, minl_p)
+
+    l = max(self.getParameter("length"), minl)
+
+    ml = getDim(servo, "motorlength")
+    sl = getDim(servo, "shoulderlength")
+    ho = getDim(servo, "hornoffset")
+
+    s = self.getParameter("shift")
+
+    dy = l/2. - ml/2. - sl
+    if self.getParameter("center"):
+      dy = min(dy, ml/2. - ho)
+    dy -= s
+    if self.getParameter("flip"):
+      dy = -dy
+
+    self.setParameter("offset", (0, dy))
+
+
+if __name__ == "__main__":
+  ServoMount.test()
+
diff --git a/rocolib/library/ServoMount.yaml b/rocolib/library/ServoMount.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ad59baa740cf5eea32abeb575db12c1e872c0a5a
--- /dev/null
+++ b/rocolib/library/ServoMount.yaml
@@ -0,0 +1,247 @@
+connections:
+  connection0:
+  - - beam
+    - face2
+  - - mount
+    - decoration
+  - mode: hole
+    offset:
+      parameter: offset
+interfaces:
+  botedge0:
+    interface: botedge0
+    subcomponent: beam
+  botedge1:
+    interface: botedge1
+    subcomponent: beam
+  botedge2:
+    interface: botedge2
+    subcomponent: beam
+  botedge3:
+    interface: botedge3
+    subcomponent: beam
+  face0:
+    interface: face0
+    subcomponent: beam
+  face1:
+    interface: face1
+    subcomponent: beam
+  face2:
+    interface: face2
+    subcomponent: beam
+  face3:
+    interface: face3
+    subcomponent: beam
+  mount.decoration:
+    interface: decoration
+    subcomponent: mount
+  slotedge:
+    interface: slotedge
+    subcomponent: beam
+  tabedge:
+    interface: tabedge
+    subcomponent: beam
+  topedge0:
+    interface: topedge0
+    subcomponent: beam
+  topedge1:
+    interface: topedge1
+    subcomponent: beam
+  topedge2:
+    interface: topedge2
+    subcomponent: beam
+  topedge3:
+    interface: topedge3
+    subcomponent: beam
+parameters:
+  _dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  addTabs:
+    defaultValue: true
+    spec:
+      valueType: bool
+  angle:
+    defaultValue: null
+    spec:
+      optional: true
+      overrides:
+      - tangle
+      - bangle
+  bangle:
+    defaultValue: 135
+    spec:
+      maxValue: 180
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  center:
+    defaultValue: true
+    spec:
+      valueType: bool
+  depth:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  flip:
+    defaultValue: false
+    spec:
+      valueType: bool
+  length:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  mindepth:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  minlength:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  minwidth:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  offset:
+    defaultValue: null
+    spec:
+      optional: true
+  phase:
+    defaultValue: 0
+    spec:
+      valueType: int
+  root:
+    defaultValue: null
+    spec:
+      optional: true
+      valueType: int
+  servo:
+    defaultValue: fs90r
+    spec:
+      valueType: str
+  shift:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  tangle:
+    defaultValue: 80
+    spec:
+      maxValue: 180
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  width:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/ServoMountBuilder.py
+subcomponents:
+  beam:
+    classname: RectBeam
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      addTabs:
+        parameter: addTabs
+      angle:
+        parameter: angle
+      bangle:
+        parameter: bangle
+      depth:
+        parameter: width
+      length:
+        parameter: length
+      mindepth:
+        function: getDim(x, "motorheight")
+        parameter: servo
+      minlength:
+        function: getDim(x, "motorlength") + getDim(x ,"shoulderlength") * 2
+        parameter: servo
+      minwidth:
+        function: getDim(x, "motorwidth")
+        parameter: servo
+      phase:
+        parameter: phase
+      root:
+        parameter: root
+      tangle:
+        parameter: tangle
+      width:
+        parameter: depth
+  mount:
+    classname: Cutout
+    kwargs: {}
+    parameters:
+      dx:
+        function: getDim(x, "motorwidth") * 0.99
+        parameter: servo
+      dy:
+        function: getDim(x, "motorlength")
+        parameter: servo
diff --git a/rocolib/library/SimpleChair.yaml b/rocolib/library/SimpleChair.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3756a211f2a105b26ef98575405efa90ef6af541
--- /dev/null
+++ b/rocolib/library/SimpleChair.yaml
@@ -0,0 +1,177 @@
+connections:
+  connection0:
+  - - seat
+    - left
+  - - legl
+    - topedge
+  - angle: 0
+  connection1:
+  - - seat
+    - right
+  - - legr
+    - topedge
+  - angle: 0
+interfaces: {}
+parameters:
+  _dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  backheight:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  gapheight:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  recline:
+    defaultValue: 110
+    spec:
+      maxValue: 360
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  taper:
+    defaultValue: 0.5
+    spec:
+      maxValue: 1
+      minValue: 0
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 70
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/SimpleChairBuilder.py
+subcomponents:
+  legl:
+    classname: VLeg
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      height:
+        parameter: height
+      taper:
+        parameter: taper
+      thickness:
+        parameter: thickness
+      width:
+        parameter: depth
+  legr:
+    classname: VLeg
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      height:
+        parameter: height
+      taper:
+        parameter: taper
+      thickness:
+        parameter: thickness
+      width:
+        parameter: depth
+  seat:
+    classname: ChairSeat
+    kwargs:
+      root: true
+    parameters:
+      backheight:
+        parameter: backheight
+      depth:
+        parameter: depth
+      gapheight:
+        parameter: gapheight
+      recline:
+        parameter: recline
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
diff --git a/rocolib/library/SimpleTable.yaml b/rocolib/library/SimpleTable.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f377c9740c7f777beb667cda5e00dfa87c7e95ef
--- /dev/null
+++ b/rocolib/library/SimpleTable.yaml
@@ -0,0 +1,238 @@
+connections:
+  connection0:
+  - - top
+    - l
+  - - legl
+    - topedge
+  - angle: 90
+  connection1:
+  - - top
+    - r
+  - - legr
+    - topedge
+  - angle: 90
+  connection2:
+  - - top
+    - t
+  - - legt
+    - topedge
+  - angle: 90
+  connection3:
+  - - top
+    - b
+  - - legb
+    - topedge
+  - angle: 90
+  connection4:
+  - - legl
+    - rightedge
+  - - legb
+    - leftedge
+  - angle: 90
+  connection5:
+  - - legb
+    - rightedge
+  - - legr
+    - leftedge
+  - angle: 90
+  connection6:
+  - - legr
+    - rightedge
+  - - legt
+    - leftedge
+  - angle: 90
+  connection7:
+  - - legl
+    - leftedge
+  - - legt
+    - rightedge
+  - angle: 90
+interfaces: {}
+parameters:
+  _dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  height:
+    defaultValue: 40
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 70
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  taper:
+    defaultValue: 0.5
+    spec:
+      maxValue: 1
+      minValue: 0
+      valueType: (float, int)
+  thickness:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/SimpleTableBuilder.py
+subcomponents:
+  legb:
+    classname: VLeg
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      height:
+        parameter: height
+      taper:
+        parameter: taper
+      thickness:
+        parameter: thickness
+      width:
+        parameter: length
+  legl:
+    classname: VLeg
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      height:
+        parameter: height
+      taper:
+        parameter: taper
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
+  legr:
+    classname: VLeg
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      height:
+        parameter: height
+      taper:
+        parameter: taper
+      thickness:
+        parameter: thickness
+      width:
+        parameter: width
+  legt:
+    classname: VLeg
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      height:
+        parameter: height
+      taper:
+        parameter: taper
+      thickness:
+        parameter: thickness
+      width:
+        parameter: length
+  top:
+    classname: Rectangle
+    kwargs:
+      root: true
+    parameters:
+      l:
+        parameter: length
+      w:
+        parameter: width
diff --git a/rocolib/library/SimpleUChannel.py b/rocolib/library/SimpleUChannel.py
new file mode 100644
index 0000000000000000000000000000000000000000..647a60437a3df50280d60064ef6c3bdda317cf65
--- /dev/null
+++ b/rocolib/library/SimpleUChannel.py
@@ -0,0 +1,59 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle
+import rocolib.utils.numsym as np
+
+
+class SimpleUChannel(FoldedComponent):
+  def define(self):
+    self.addParameter("length", 100, paramType="length")
+    self.addParameter("width", 50, paramType="length")
+    self.addParameter("depth", 20, paramType="length")
+
+    for i in range(3):
+      self.addEdgeInterface("topedge%d" % i, "r%d.e0" % i, ["depth", "width"][i % 2])
+      self.addEdgeInterface("botedge%d" % i, "r%d.e2" % i, ["depth", "width"][i % 2])
+      self.addFaceInterface("face%d" % i, "r%d" % i)
+
+    self.addEdgeInterface("ledge", "r0.e3", "length")
+    self.addEdgeInterface("redge", "r2.e1", "length")
+
+    self.addEdgeInterface("top", ["r%d.e0" % i for i in range(3)], ["depth", "width", "depth"])
+    self.addEdgeInterface("bot", ["r%d.e2" % (2-i) for i in range(3)], ["depth", "width", "depth"])
+
+  def assemble(self):
+    length = self.getParameter("length")
+    width = self.getParameter("width")
+    depth = self.getParameter("depth")
+
+    rs = []
+    rs.append(Rectangle("", depth, length))
+    rs.append(Rectangle("", width, length))
+    rs.append(Rectangle("", depth, length))
+
+    fromEdge = None
+    for i in range(3):
+      self.attachEdge(fromEdge, rs[i], "e3", prefix="r%d"%i, angle=90, root=(i==1))
+      fromEdge = 'r%d.e1' % i
+
+
+if __name__ == "__main__":
+  SimpleUChannel.test()
+
+  #test transform3D
+  r = SimpleUChannel()
+  r.makeOutput(transform3D=[[1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]], default=False)
+  g = r.getGraph()
+  for f in g.faces:
+    print ("########", f.name)
+    print(f.get3DCoords())
+
+  #test sympy
+  r = SimpleUChannel()
+  r.makeOutput(useDefaultParameters=False, default=False)
+  g = r.getGraph()
+  for f in g.faces:
+    print ("########", f.name)
+    #print(f.transform2D)
+    #print(f.transform3D)
+    #print(f.get2DCoords())
+    np.pprint(f.get3DCoords())
diff --git a/rocolib/library/SplitEdge.py b/rocolib/library/SplitEdge.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b1f527a64e594c54a1868d01e95f641c8a2dc81
--- /dev/null
+++ b/rocolib/library/SplitEdge.py
@@ -0,0 +1,41 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face
+from rocolib.utils.numsym import cumsum
+
+
+def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
+    return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+class SplitEdge(FoldedComponent):
+    def define(self):
+        self.addParameter("toplength", (50, 50), valueType="(tuple, list)")
+        self.addParameter("botlength", (100,), valueType="(tuple, list)")
+        self.addParameter("width", 0)
+
+        for i in range(100):
+          self.addEdgeInterface("topedge%d" % i, None, "toplength")
+          self.addEdgeInterface("botedge%d" % i, None, "botlength")
+
+    def assemble(self):
+        t = cumsum(self.getParameter("toplength")[::-1])
+        b = cumsum(self.getParameter("botlength")[::-1])
+        if not isclose(t[-1], b[-1]):
+          raise ValueError("SplitEdge lengths not equal: %s <> %s" % (repr(t), repr(b)))
+
+        w = self.getParameter("width")
+        pts = [(x, 0) for x in b]
+        pts += [(x, w) for x in t[::-1]]
+        pts += [(0, w), (0,0)]
+
+        self.addFace(Face("split", pts))
+
+        tops = ["e%d" % (len(b) + d + 1) for d in range(len(t))]
+        bots = ["e%d" % d for d in range(len(b))[::-1]]
+        for i, topedge in enumerate(tops):
+          self.setEdgeInterface("topedge%d" % i, topedge, "toplength")
+        for i, botedge in enumerate(bots):
+          self.setEdgeInterface("botedge%d" % i, botedge, "botlength")
+
+if __name__ == "__main__":
+    SplitEdge.test()
+
diff --git a/rocolib/library/Stool.py b/rocolib/library/Stool.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bf5b6ed795df83b51f9658e7dff1d27e4a7d147
--- /dev/null
+++ b/rocolib/library/Stool.py
@@ -0,0 +1,35 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle, RegularNGon
+from rocolib.api.composables.graph.Face import RegularNGon2 as r2l
+from rocolib.utils.numsym import sin, deg2rad
+
+
+class Stool(FoldedComponent):
+    def define(self):
+        self.addParameter("height", 60, paramType="length", minValue=10)
+        self.addParameter("legs", 3, paramType="count", minValue=1)
+        self.addParameter("radius", optional=True, overrides=("legwidth",))
+        self.addParameter("legwidth", 20, paramType="length", minValue=10)
+        self.addParameter("angle", 80, paramType="angle")
+
+    def modifyParameters(self):
+        if self.getParameter("radius") is not None:
+          self.setParameter("legwidth", r2l.r2l(self.getParameter("radius"), self.getParameter("legs")*2))
+
+    def assemble(self):
+        h = self.getParameter("height")
+        lp = self.getParameter("legs")
+        e = self.getParameter("legwidth")
+        ap = self.getParameter("angle")
+
+        n = lp * 2
+        self.addFace(RegularNGon("", n, e), "seat")
+
+        for i in range(0, int(n/2)):
+            s = Rectangle("", e, h)
+            self.attachEdge("seat.e%d" % (2*i), s, "e0", "leg%d" % i, angle=ap)
+
+if __name__ == "__main__":
+    from rocolib.api.composables.graph.Joint import FingerJoint
+    Stool.test(thickness=4, joint=FingerJoint(thickness=4))
+
diff --git a/rocolib/library/Tail.py b/rocolib/library/Tail.py
new file mode 100644
index 0000000000000000000000000000000000000000..20671275919b194bbe6188114805adfa6b160108
--- /dev/null
+++ b/rocolib/library/Tail.py
@@ -0,0 +1,42 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle, RightTriangle
+
+
+class Tail(FoldedComponent):
+    def define(self):
+        self.addParameter("height", 60, paramType="length")
+        self.addParameter("depth", 20, paramType="length")
+        self.addParameter("width", 80, paramType="length")
+        self.addParameter("flapwidth", 0.2, paramType="ratio")
+        self.addParameter("tailwidth", 1/3, paramType="ratio")
+
+        self.addEdgeInterface("topedge", "spacer.e4", "width")
+        self.addEdgeInterface("flapedge", "tail.e2", "width")
+
+
+    def assemble(self):
+        h = self.getParameter("height")
+        w = self.getParameter("width")
+        d = self.getParameter("depth")
+
+        flapwidth = self.getParameter("flapwidth")
+        tailwidth = self.getParameter("tailwidth")
+        lr0 = (1-flapwidth)/2.
+        lr1 = 1-lr0
+        lt0 = (1-tailwidth)/2.
+        lt1 = 1-lt0
+
+        s = Face("", ((w*lr0, 0), (w*lr1, 0), (w, 0), (w, 0.1), (0, 0.1), (0, 0)))
+        t = Face("", ((w*lr0, 0), (w*lr0, d), (w*lr1, d), (w*lr1, 0), (w, 0), (w*lt1, h), (w*lt0, h), (0, 0)))
+        f1 = RightTriangle("", h, w*lt0)
+        f2 = RightTriangle("", w*lt0, h)
+
+        self.addFace(s, "spacer")
+        self.attachEdge("spacer.e2", t, "e0", "tail", angle=0)
+        self.mergeEdge("spacer.e0", "tail.e4", angle=0)
+        self.attachEdge("tail.e5", f2, "e1", "f1", angle=-135)
+        self.attachEdge("tail.e7", f1, "e1", "f2", angle=-135)
+
+if __name__ == "__main__":
+    Tail.test()
+
diff --git a/rocolib/library/Trimaran.yaml b/rocolib/library/Trimaran.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..032a1463695d19053944a5cebb14d459c10cd91f
--- /dev/null
+++ b/rocolib/library/Trimaran.yaml
@@ -0,0 +1,664 @@
+connections:
+  connection0:
+  - - portsplit0
+    - botedge0
+  - - boat0
+    - portedge
+  - angle: -90
+  connection1:
+  - - starsplit0
+    - topedge0
+  - - boat0
+    - staredge
+  - angle: -90
+  connection10:
+  - - starsplit1
+    - botedge2
+  - - seat2
+    - l
+  - {}
+  connection11:
+  - - portsplit2
+    - topedge2
+  - - seat2
+    - r
+  - {}
+  connection12:
+  - - starsplit0
+    - botedge3
+  - - seat3
+    - l
+  - {}
+  connection13:
+  - - portsplit1
+    - topedge3
+  - - seat3
+    - r
+  - {}
+  connection14:
+  - - starsplit1
+    - botedge4
+  - - seat4
+    - l
+  - {}
+  connection15:
+  - - portsplit2
+    - topedge4
+  - - seat4
+    - r
+  - {}
+  connection16:
+  - - starsplit0
+    - botedge5
+  - - seat5
+    - l
+  - {}
+  connection17:
+  - - portsplit1
+    - topedge5
+  - - seat5
+    - r
+  - {}
+  connection18:
+  - - starsplit1
+    - botedge6
+  - - seat6
+    - l
+  - {}
+  connection19:
+  - - portsplit2
+    - topedge6
+  - - seat6
+    - r
+  - {}
+  connection2:
+  - - portsplit1
+    - botedge0
+  - - boat1
+    - portedge
+  - angle: -90
+  connection20:
+  - - starsplit0
+    - botedge7
+  - - seat7
+    - l
+  - {}
+  connection21:
+  - - portsplit1
+    - topedge7
+  - - seat7
+    - r
+  - {}
+  connection22:
+  - - starsplit1
+    - botedge8
+  - - seat8
+    - l
+  - {}
+  connection23:
+  - - portsplit2
+    - topedge8
+  - - seat8
+    - r
+  - {}
+  connection24:
+  - - starsplit0
+    - botedge9
+  - - seat9
+    - l
+  - {}
+  connection25:
+  - - portsplit1
+    - topedge9
+  - - seat9
+    - r
+  - {}
+  connection3:
+  - - starsplit1
+    - topedge0
+  - - boat1
+    - staredge
+  - angle: -90
+  connection4:
+  - - portsplit2
+    - botedge0
+  - - boat2
+    - portedge
+  - angle: -90
+  connection5:
+  - - starsplit2
+    - topedge0
+  - - boat2
+    - staredge
+  - angle: -90
+  connection6:
+  - - starsplit1
+    - botedge0
+  - - seat0
+    - l
+  - {}
+  connection7:
+  - - portsplit2
+    - topedge0
+  - - seat0
+    - r
+  - {}
+  connection8:
+  - - starsplit0
+    - botedge1
+  - - seat1
+    - l
+  - {}
+  connection9:
+  - - portsplit1
+    - topedge1
+  - - seat1
+    - r
+  - {}
+interfaces: {}
+parameters:
+  boat._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  boat._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  boat.depth:
+    defaultValue: 20
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  boat.length:
+    defaultValue: 100
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  boat.width:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  bow._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  bow._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  bow.point:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  seats:
+    defaultValue: 6
+    spec:
+      maxValue: 10
+      minValue: 2
+      valueType: int
+  spacing:
+    defaultValue: 25
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  stern._dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  stern._q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern._q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  stern.point:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/TrimaranBuilder.py
+subcomponents:
+  boat0:
+    classname: BoatBase
+    kwargs:
+      root: true
+    parameters:
+      boat._dx:
+        parameter: boat._dx
+      boat._dy:
+        parameter: boat._dy
+      boat._dz:
+        parameter: boat._dz
+      boat._q_a:
+        parameter: boat._q_a
+      boat._q_i:
+        parameter: boat._q_i
+      boat._q_j:
+        parameter: boat._q_j
+      boat._q_k:
+        parameter: boat._q_k
+      boat.depth:
+        parameter: boat.depth
+      boat.length:
+        parameter: boat.length
+      boat.width:
+        parameter: boat.width
+      bow._dx:
+        parameter: bow._dx
+      bow._dy:
+        parameter: bow._dy
+      bow._dz:
+        parameter: bow._dz
+      bow._q_a:
+        parameter: bow._q_a
+      bow._q_i:
+        parameter: bow._q_i
+      bow._q_j:
+        parameter: bow._q_j
+      bow._q_k:
+        parameter: bow._q_k
+      bow.point:
+        parameter: bow.point
+      stern._dx:
+        parameter: stern._dx
+      stern._dy:
+        parameter: stern._dy
+      stern._dz:
+        parameter: stern._dz
+      stern._q_a:
+        parameter: stern._q_a
+      stern._q_i:
+        parameter: stern._q_i
+      stern._q_j:
+        parameter: stern._q_j
+      stern._q_k:
+        parameter: stern._q_k
+      stern.point:
+        parameter: stern.point
+  boat1:
+    classname: BoatBase
+    kwargs:
+      root: true
+    parameters:
+      boat._dx:
+        parameter: boat._dx
+      boat._dy:
+        parameter: boat._dy
+      boat._dz:
+        parameter: boat._dz
+      boat._q_a:
+        parameter: boat._q_a
+      boat._q_i:
+        parameter: boat._q_i
+      boat._q_j:
+        parameter: boat._q_j
+      boat._q_k:
+        parameter: boat._q_k
+      boat.depth:
+        parameter: boat.depth
+      boat.length:
+        parameter: boat.length
+      boat.width:
+        parameter: boat.width
+      bow._dx:
+        parameter: bow._dx
+      bow._dy:
+        parameter: bow._dy
+      bow._dz:
+        parameter: bow._dz
+      bow._q_a:
+        parameter: bow._q_a
+      bow._q_i:
+        parameter: bow._q_i
+      bow._q_j:
+        parameter: bow._q_j
+      bow._q_k:
+        parameter: bow._q_k
+      bow.point:
+        parameter: bow.point
+      stern._dx:
+        parameter: stern._dx
+      stern._dy:
+        parameter: stern._dy
+      stern._dz:
+        parameter: stern._dz
+      stern._q_a:
+        parameter: stern._q_a
+      stern._q_i:
+        parameter: stern._q_i
+      stern._q_j:
+        parameter: stern._q_j
+      stern._q_k:
+        parameter: stern._q_k
+      stern.point:
+        parameter: stern.point
+  boat2:
+    classname: BoatBase
+    kwargs:
+      root: true
+    parameters:
+      boat._dx:
+        parameter: boat._dx
+      boat._dy:
+        parameter: boat._dy
+      boat._dz:
+        parameter: boat._dz
+      boat._q_a:
+        parameter: boat._q_a
+      boat._q_i:
+        parameter: boat._q_i
+      boat._q_j:
+        parameter: boat._q_j
+      boat._q_k:
+        parameter: boat._q_k
+      boat.depth:
+        parameter: boat.depth
+      boat.length:
+        parameter: boat.length
+      boat.width:
+        parameter: boat.width
+      bow._dx:
+        parameter: bow._dx
+      bow._dy:
+        parameter: bow._dy
+      bow._dz:
+        parameter: bow._dz
+      bow._q_a:
+        parameter: bow._q_a
+      bow._q_i:
+        parameter: bow._q_i
+      bow._q_j:
+        parameter: bow._q_j
+      bow._q_k:
+        parameter: bow._q_k
+      bow.point:
+        parameter: bow.point
+      stern._dx:
+        parameter: stern._dx
+      stern._dy:
+        parameter: stern._dy
+      stern._dz:
+        parameter: stern._dz
+      stern._q_a:
+        parameter: stern._q_a
+      stern._q_i:
+        parameter: stern._q_i
+      stern._q_j:
+        parameter: stern._q_j
+      stern._q_k:
+        parameter: stern._q_k
+      stern.point:
+        parameter: stern.point
+  portsplit0:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[x[0]]'
+        parameter: &id001
+        - boat.length
+        - seats
+      toplength:
+        function: '[x[0]/(1.*x[1])] * x[1]'
+        parameter: *id001
+  portsplit1:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[x[0]]'
+        parameter: *id001
+      toplength:
+        function: '[x[0]/(1.*x[1])] * x[1]'
+        parameter: *id001
+  portsplit2:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[x[0]]'
+        parameter: *id001
+      toplength:
+        function: '[x[0]/(1.*x[1])] * x[1]'
+        parameter: *id001
+  seat0:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (0 < x[1]) and x[0] or 0
+        parameter: &id002
+        - spacing
+        - seats
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat1:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (1 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat2:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (2 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat3:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (3 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat4:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (4 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat5:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (5 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat6:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (6 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat7:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (7 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat8:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (8 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  seat9:
+    classname: Rectangle
+    kwargs: {}
+    parameters:
+      l:
+        function: (9 < x[1]) and x[0] or 0
+        parameter: *id002
+      w:
+        function: x[0]/(1.*x[1])
+        parameter: *id001
+  starsplit0:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[x[0]/(1.*x[1])] * x[1]'
+        parameter: *id001
+      toplength:
+        function: '[x[0]]'
+        parameter: *id001
+  starsplit1:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[x[0]/(1.*x[1])] * x[1]'
+        parameter: *id001
+      toplength:
+        function: '[x[0]]'
+        parameter: *id001
+  starsplit2:
+    classname: SplitEdge
+    kwargs: {}
+    parameters:
+      botlength:
+        function: '[x[0]/(1.*x[1])] * x[1]'
+        parameter: *id001
+      toplength:
+        function: '[x[0]]'
+        parameter: *id001
diff --git a/rocolib/library/Tug.yaml b/rocolib/library/Tug.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7444ea8f2200f4079955451b694a976cad470e16
--- /dev/null
+++ b/rocolib/library/Tug.yaml
@@ -0,0 +1,75 @@
+connections:
+  connection0:
+  - - cabin
+    - portedge
+  - - boat
+    - portedge
+  - angle: 0
+  connection1:
+  - - cabin
+    - staredge
+  - - boat
+    - staredge
+  - angle: 0
+    tabWidth: 10
+interfaces: {}
+parameters:
+  depth:
+    defaultValue: 50
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  height:
+    defaultValue: 30
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  length:
+    defaultValue: 200
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  width:
+    defaultValue: 60
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/TugBuilder.py
+subcomponents:
+  boat:
+    classname: BoatBase
+    kwargs:
+      root: true
+    parameters:
+      boat.depth:
+        function: x/3.
+        parameter: width
+      boat.length:
+        function: sum(x)
+        parameter:
+        - length
+        - depth
+      boat.width:
+        parameter: width
+      bow.point:
+        function: x/2.
+        parameter: length
+      stern.point:
+        function: x/8.
+        parameter: length
+  cabin:
+    classname: Cabin
+    kwargs: {}
+    parameters:
+      depth:
+        parameter: depth
+      height:
+        parameter: height
+      length:
+        parameter: length
+      width:
+        parameter: width
diff --git a/rocolib/library/TwoNGons.py b/rocolib/library/TwoNGons.py
new file mode 100644
index 0000000000000000000000000000000000000000..cacc3ee962bb2d366ddaaa70bc581f28a86eab7e
--- /dev/null
+++ b/rocolib/library/TwoNGons.py
@@ -0,0 +1,23 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import RegularNGon2 as Shape
+from rocolib.utils.transforms import Translate
+
+
+class TwoNGons(FoldedComponent):
+  def define(self):
+    self.addParameter("n1", 5, valueType="int", minValue=3)
+    self.addParameter("n2", 3, valueType="int", minValue=3)
+    self.addParameter("d", 10, paramType="length")
+    self.addParameter("radius", 25, paramType="length")
+
+  def assemble(self):
+    n1 = self.getParameter("n1")
+    n2 = self.getParameter("n2")
+    d = self.getParameter("d")
+    l = self.getParameter("radius")
+
+    self.addFace(Shape("", n1, l), "r1")
+    self.attachFace("r1", Shape("", n2, l), "r2", Translate([0,0,d]))
+
+if __name__ == "__main__":
+    TwoNGons.test()
diff --git a/rocolib/library/UChannel.py b/rocolib/library/UChannel.py
new file mode 100644
index 0000000000000000000000000000000000000000..010014daf61e217b8a4fe479642f6c95091e4f8f
--- /dev/null
+++ b/rocolib/library/UChannel.py
@@ -0,0 +1,83 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle
+import rocolib.utils.numsym as np
+
+class UChannel(FoldedComponent):
+  def define(self):
+    self.addParameter("length", 100, paramType="length")
+    self.addParameter("width", 50, paramType="length")
+    self.addParameter("depth", 20, paramType="length")
+
+    # Minimum of 45deg to make sure the geometry stays convex
+    self.addParameter("angle", optional=True, overrides=("tangle", "bangle"))
+    self.addParameter("tangle", 80, paramType="angle", minValue=45)
+    self.addParameter("bangle", 135, paramType="angle", minValue=45)
+
+    for i in range(3):
+      self.addEdgeInterface("topedge%d" % i, "r%d.e0" % i, ["depth", "width"][i % 2])
+      self.addEdgeInterface("botedge%d" % i, "r%d.e2" % i, ["depth", "width"][i % 2])
+      self.addFaceInterface("face%d" % i, "r%d" % i)
+    self.addEdgeInterface("top", ["r%d.e0" % i for i in range(3)], ["depth", "width", "depth"])
+    self.addEdgeInterface("bot", ["r%d.e2" % (2-i) for i in range(3)], ["depth", "width", "depth"])
+
+    self.addEdgeInterface("ledge", "r0.e3", "length")
+    self.addEdgeInterface("redge", "r2.e1", "length")
+
+  def modifyParameters(self):
+    if self.getParameter("angle") is not None:
+      self.setParameter("bangle", self.getParameter("angle"))
+      self.setParameter("tangle", self.getParameter("angle"))
+
+  def assemble(self):
+    bangle = 90 - self.getParameter("bangle")
+    tangle = 90 - self.getParameter("tangle")
+
+    length = self.getParameter("length")
+    width = self.getParameter("width")
+    depth = self.getParameter("depth")
+
+    def dl(a):
+        return np.tan(np.deg2rad(a)) * depth
+
+    rs = []
+    rs.append(Face("", (
+      (depth, dl(tangle)),
+      (depth, length - dl(bangle)),
+      (0, length), 
+      (0,0)
+    )))
+    rs.append(Rectangle("", width, length - dl(tangle) - dl(bangle)))
+    rs.append(Face("", (
+      (0, length), 
+      (0,0),
+      (depth, dl(bangle)),
+      (depth, length - dl(tangle))
+    )))
+
+    fromEdge = None
+    for i in range(3):
+      self.attachEdge(fromEdge, rs[i], "e3", prefix="r%d"%i, angle=90, root=(i==1))
+      fromEdge = 'r%d.e1' % i
+
+if __name__ == "__main__":
+  UChannel.test()
+
+  #test transform3D
+  r = UChannel()
+  r.makeOutput(transform3D=[[1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]], default=False)
+  g = r.getGraph()
+  for f in g.faces:
+    print ("########", f.name)
+    print(f.get3DCoords())
+
+  #test sympy
+  r = UChannel()
+  r.setParameter("angle", 90)
+  r.makeOutput(useDefaultParameters=False, default=False)
+  g = r.getGraph()
+  for f in g.faces:
+    print ("########", f.name)
+    #print(f.transform2D)
+    #print(f.transform3D)
+    #print(f.get2DCoords())
+    np.pprint(f.get3DCoords())
diff --git a/rocolib/library/VLeg.py b/rocolib/library/VLeg.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f77bd88ae1407553f540e06d3feff9f63c4c3d0
--- /dev/null
+++ b/rocolib/library/VLeg.py
@@ -0,0 +1,27 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face
+
+
+class VLeg(FoldedComponent):
+    def define(self):
+        self.addParameter("height", 40, paramType="length")
+        self.addParameter("width", 50, paramType="length")
+        self.addParameter("thickness", 10, paramType="length")
+        self.addParameter("taper", 0.5, paramType="ratio")
+
+        self.addEdgeInterface("leftedge", "leg.e0", "height")
+        self.addEdgeInterface("topedge", "leg.e1", "width")
+        self.addEdgeInterface("rightedge", "leg.e2", "height")
+
+    def assemble(self):
+        h = self.getParameter("height")
+        w = self.getParameter("width")
+        t = self.getParameter("thickness")
+        r = self.getParameter("taper") * t
+
+        s = Face("", ((h, 0), (h, w), (0, w), (0, w-r), (h-t, w-t), (h-t, t), (0, r), (0,0)))
+        self.addFace(s, "leg")
+
+if __name__ == "__main__":
+    VLeg.test()
+
diff --git a/rocolib/library/Wheel.yaml b/rocolib/library/Wheel.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..97a7440c022656b32a30f5a85d3898f18fafed98
--- /dev/null
+++ b/rocolib/library/Wheel.yaml
@@ -0,0 +1,261 @@
+connections:
+  connection0:
+  - - drive
+    - mount
+  - - tire
+    - face
+  - {}
+interfaces:
+  botedge0:
+    interface: botedge0
+    subcomponent: drive
+  botedge1:
+    interface: botedge1
+    subcomponent: drive
+  botedge2:
+    interface: botedge2
+    subcomponent: drive
+  botedge3:
+    interface: botedge3
+    subcomponent: drive
+  face0:
+    interface: face0
+    subcomponent: drive
+  face1:
+    interface: face1
+    subcomponent: drive
+  face2:
+    interface: face2
+    subcomponent: drive
+  face3:
+    interface: face3
+    subcomponent: drive
+  horn:
+    interface: horn
+    subcomponent: drive
+  mount:
+    interface: mount
+    subcomponent: drive
+  mount.decoration:
+    interface: mount.decoration
+    subcomponent: drive
+  slotedge:
+    interface: slotedge
+    subcomponent: drive
+  tabedge:
+    interface: tabedge
+    subcomponent: drive
+  topedge0:
+    interface: topedge0
+    subcomponent: drive
+  topedge1:
+    interface: topedge1
+    subcomponent: drive
+  topedge2:
+    interface: topedge2
+    subcomponent: drive
+  topedge3:
+    interface: topedge3
+    subcomponent: drive
+parameters:
+  _dx:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dy:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _dz:
+    defaultValue: 0
+    spec:
+      minValue: null
+      units: mm
+      valueType: (float, int)
+  _q_a:
+    defaultValue: 1
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_i:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_j:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  _q_k:
+    defaultValue: 0
+    spec:
+      maxValue: 1
+      minValue: -1
+      valueType: (int, float)
+  addTabs:
+    defaultValue: true
+    spec:
+      valueType: bool
+  angle:
+    defaultValue: null
+    spec:
+      optional: true
+      overrides:
+      - tangle
+      - bangle
+  bangle:
+    defaultValue: 135
+    spec:
+      maxValue: 180
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  center:
+    defaultValue: true
+    spec:
+      valueType: bool
+  depth:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  flip:
+    defaultValue: false
+    spec:
+      valueType: bool
+  length:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  mindepth:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  minlength:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  minwidth:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  offset:
+    defaultValue: null
+    spec:
+      optional: true
+  phase:
+    defaultValue: 0
+    spec:
+      valueType: int
+  radius:
+    defaultValue: 25
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  root:
+    defaultValue: null
+    spec:
+      optional: true
+      valueType: int
+  servo:
+    defaultValue: fs90r
+    spec:
+      valueType: str
+  shift:
+    defaultValue: 0
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+  tangle:
+    defaultValue: 80
+    spec:
+      maxValue: 180
+      minValue: 0
+      units: degrees
+      valueType: (float, int)
+  width:
+    defaultValue: 10
+    spec:
+      minValue: 0
+      units: mm
+      valueType: (float, int)
+source: ../builders/WheelBuilder.py
+subcomponents:
+  drive:
+    classname: MountedServo
+    kwargs: {}
+    parameters:
+      _dx:
+        parameter: _dx
+      _dy:
+        parameter: _dy
+      _dz:
+        parameter: _dz
+      _q_a:
+        parameter: _q_a
+      _q_i:
+        parameter: _q_i
+      _q_j:
+        parameter: _q_j
+      _q_k:
+        parameter: _q_k
+      addTabs:
+        parameter: addTabs
+      angle:
+        parameter: angle
+      bangle:
+        parameter: bangle
+      center:
+        parameter: center
+      depth:
+        parameter: depth
+      flip:
+        parameter: flip
+      length:
+        parameter: length
+      mindepth:
+        parameter: mindepth
+      minlength:
+        parameter: minlength
+      minwidth:
+        parameter: minwidth
+      offset:
+        parameter: offset
+      phase:
+        parameter: phase
+      root:
+        parameter: root
+      servo:
+        parameter: servo
+      shift:
+        parameter: shift
+      tangle:
+        parameter: tangle
+      width:
+        parameter: width
+  tire:
+    classname: RegularNGon
+    kwargs: {}
+    parameters:
+      n: 40
+      radius:
+        parameter: radius
diff --git a/rocolib/library/Wing.py b/rocolib/library/Wing.py
new file mode 100644
index 0000000000000000000000000000000000000000..9019d4b1e3dfa67e8341ac6dada645cbfe8f6aa6
--- /dev/null
+++ b/rocolib/library/Wing.py
@@ -0,0 +1,46 @@
+from rocolib.api.components import FoldedComponent
+from rocolib.api.composables.graph.Face import Face, Rectangle
+
+class Wing(FoldedComponent):
+  def define(self):
+    self.addParameter("bodylength", 50, paramType="length")
+    self.addParameter("wingspan", 100, paramType="length")
+    self.addParameter("wingtip", 10, paramType="length")
+    self.addParameter("thickness", 10, paramType="length")
+    self.addParameter("flip", False, valueType="bool")
+
+    self.addEdgeInterface("base", "bottom.e1", "bodylength")
+    self.addEdgeInterface("tip", "bottom.e3", "wingtip")
+    self.addEdgeInterface("basetop", "top.e5", 0)
+    self.addEdgeInterface("tiptop", "top.e1", 0)
+
+    self.addFaceInterface("bottom", "bottom")
+
+  def assemble(self):
+    bodylength = self.getParameter("bodylength")
+    wingspan = self.getParameter("wingspan")
+    wingtip = self.getParameter("wingtip")
+    thickness = self.getParameter("thickness")
+    flip = self.getParameter("flip")
+
+    if flip:
+        bodylength, wingtip = wingtip, bodylength
+
+    self.addFace(Face("", (
+      (wingspan, 0), (wingspan, bodylength), (0, wingtip), (0,0)
+    ), recenter=False), "bottom")
+
+    self.attachEdge("bottom.e2", Face("", (
+      (wingspan, 0), (wingspan, wingtip/2.+thickness), (wingspan, wingtip+thickness), (0, bodylength+thickness), (0, bodylength/2.+thickness), (0,0)
+    )), "e3", prefix="top", angle = 170)
+
+    self.addTab("bottom.e0", "top.e0", angle= 170, width=thickness)
+
+    if flip:
+        self.setEdgeInterface("base", "bottom.e3", "wingtip")
+        self.setEdgeInterface("tip", "bottom.e1", "bodylength")
+        self.setEdgeInterface("basetop", "top.e1", 0)
+        self.setEdgeInterface("tiptop", "top.e5", 0)
+
+if __name__ == "__main__":
+  Wing.test()
diff --git a/rocolib/library/__init__.py b/rocolib/library/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e3fbc37a3449c36dd40545e2ffb834399568d4c
--- /dev/null
+++ b/rocolib/library/__init__.py
@@ -0,0 +1,150 @@
+from os import system
+from os.path import dirname, realpath, basename, join
+from glob import glob
+import importlib
+import logging
+
+from rocolib import rocopath
+from rocolib.utils.io import load_yaml
+from rocolib.api.components import Component
+
+
+log = logging.getLogger(__name__)
+ROCOLIB_LIBRARY = dirname(realpath(__file__))
+
+pyComponents = [ basename(f)[:-3] for f in glob(ROCOLIB_LIBRARY + "/[!_]*.py")]
+yamlComponents = [ basename(f)[:-5] for f in glob(ROCOLIB_LIBRARY + "/*.yaml")]
+allComponents = list(set(pyComponents + yamlComponents))
+
+def getComponent(c, **kwargs):
+  '''
+  Here we are  doing Dynamic instantiation from string name of a class in dynamically imported module
+  Parameter c (str): component name e.g. 'Stool'
+  '''
+  if c in pyComponents:
+    # Load "module.submodule.MyClass"
+    obj = getattr(importlib.import_module("rocolib.library." + c), c)
+    # Instantiate the class (pass arguments to the constructor, if needed)
+    my_obj = obj()
+  elif c in yamlComponents:
+    my_obj = Component(f"{ROCOLIB_LIBRARY}/{c}.yaml")
+  else:
+    raise ValueError(f"Component {c} not found in library")
+
+  for k, v in kwargs.items():
+    if k == 'name':
+      my_obj.setName(v)
+    else:
+      my_obj.setParameter(k, v)
+  if 'name' not in kwargs:
+    my_obj.setName(c)
+
+  return my_obj
+
+def rebuild(built=None):
+    if built is None:
+        built = set()
+    success = True
+    for c in yamlComponents:
+        success &= rebuildComponent(c, built, throw=False)
+    assert success, "Error rebuilding all components, check stdout/stderr for details"
+
+def rebuildComponent(c, built=None, throw=True):
+    if c not in yamlComponents:
+        log.debug(f"{c} is not a yaml Component, skipping")
+        return True
+    if built is None:
+        built = set()
+    if c in built:
+        log.debug(f"{c} has been rebuilt, skipping")
+        return True
+
+    definition = load_yaml(c)
+    src = definition.get("source", None)
+    success = True
+    if src:
+        log.info(f"Rebuilding {c} from {ROCOLIB_LIBRARY}/{src}...")
+
+        subcomponents = set()
+        for k, v in definition.get("subcomponents", dict()).items():
+            subcomponents.add(v["classname"])
+        for sc in subcomponents:
+            rebuildComponent(sc, built)
+
+        # XXX TOOD: Test to make sure we don't call this script and then infinitely recurse!
+        log.debug(f"Calling os.system: % python {ROCOLIB_LIBRARY}/{src}")
+        if system(f"python {ROCOLIB_LIBRARY}/{src}"):
+            success = False
+
+        built.add(c)
+        log.debug(repr(built))
+        log.debug(f"Done rebuilding {c}.")
+    if throw:
+        assert success, f"Error rebuilding {c}, check stdout/stderr for details"
+    return success
+
+def getComponentPaths(c):
+    paths = {}
+    if c in pyComponents:
+        paths["python"] = rocopath(join(ROCOLIB_LIBRARY, f"{c}.py"))
+    if c in yamlComponents:
+        paths["yaml"] = rocopath(join(ROCOLIB_LIBRARY, f"{c}.yaml"))
+        definition = load_yaml(c)
+        src = definition.get("source", None)
+        if src:
+            paths["builder"] = rocopath(join(ROCOLIB_LIBRARY, src))
+    return paths
+
+
+# tag : [[required ports], [forbidden ports]]
+tagDefinitions = {
+  'sensor': [["DataOutputPort"],[]],
+  'actuator': [["DataInputPort"],[]],
+  'mechanical': [["EdgePort"],[]],
+  'device': [["MountPort"],[]],
+  'UI': [[],["MountPort", "EdgePort"]]
+}
+
+def tag(ports):
+  tags = {}
+  portset = set(ports.keys())
+  for tag, (must, cant) in tagDefinitions.items():
+    if set(must).issubset(portset) and not len(set(cant).intersection(portset)):
+      tags[tag] = [port for ptype in must for port in ports[ptype] ]
+  return tags
+
+_taggedComponents = {}
+def getTags(x):
+  if x in _taggedComponents:
+    return _taggedComponents[x]
+
+  try:
+    c = getComponent(x)
+  except:
+    return None
+
+  if isinstance(c, Component):
+    interfaces = list(c.interfaces.keys())
+    ports = {}
+    for iname in interfaces:
+      i = c.getInterface(iname)
+      iclass = i.__class__.__name__
+      try:
+        ports[iclass].append(iname)
+      except KeyError:
+        ports[iclass] = [iname]
+    _taggedComponents[x] = tag(ports)
+    return tag(ports)
+  return None
+
+def taggedComponents(components = None):
+  if components == None:
+    components = allComponents
+  for x in components:
+    if getTags(x):
+      yield x, getTags(x)
+
+def filterComponents(tagList, components = None):
+  for x, tags in taggedComponents(components):
+    if set(tagList).issubset(set(tags.keys())):
+      yield x, [port for tag in tagList for port in tags[tag] ]
diff --git a/rocolib/test/pytest.ini b/rocolib/test/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..b51f49847e466ec1834d069ee1da0cda75fc4f9b
--- /dev/null
+++ b/rocolib/test/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+addopts=-r fEpP
diff --git a/rocolib/test/test_library.py b/rocolib/test/test_library.py
new file mode 100644
index 0000000000000000000000000000000000000000..4fe21d84cd1dfcf370ce7aa291bc443537030f95
--- /dev/null
+++ b/rocolib/test/test_library.py
@@ -0,0 +1,22 @@
+import pytest
+import logging
+from rocolib.library import getComponent, pyComponents, yamlComponents, allComponents, rebuild, rebuildComponent
+
+
+def test_rebuild(c = None):
+    if c:
+        rebuildComponent(c)
+    else:
+        rebuild()
+
+@pytest.mark.parametrize("component",pyComponents)
+def test_component_python(component):
+    getComponent(component).test()
+
+@pytest.mark.parametrize("component",yamlComponents)
+def test_component_yaml(component):
+    getComponent(component).makeOutput(default=False)
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
+    test_rebuild()
diff --git a/rocolib/utils/__init__.py b/rocolib/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/rocolib/utils/dimensions.py b/rocolib/utils/dimensions.py
new file mode 100644
index 0000000000000000000000000000000000000000..931dedae2425d251a2bcdb19704b3825d424d986
--- /dev/null
+++ b/rocolib/utils/dimensions.py
@@ -0,0 +1,145 @@
+from sympy import symbols
+
+dims = {}
+
+def isDim(obj):
+    return obj in dims
+
+def getDim(obj, param):
+    return dims[obj][param]
+
+'''
+Brain dimension parameters:
+      params.setdefault("length")
+      params.setdefault("width")
+      params.setdefault("height")
+
+      params.setdefault("nrows")
+      params.setdefault("ncols")
+      params.setdefault("rowsep")
+      params.setdefault("colsep")
+'''
+dims["proMini"] = { "type" : "brains",
+    "length"    : 39,
+    "width"     : 19,
+    "height"    : 9,
+    "nrows"     : 12,
+    "ncols"     : 2,
+    "rowsep"    : 0.1 * 25.4,
+    "colsep"    : 0.6 * 25.4,
+}
+
+dims["nodeMCU"] = { "type" : "brains",
+    "length"    : 59.5,
+    "width"     : 44,
+    "height"    : 13,
+    "nrows"     : 15,
+    "ncols"     : 2,
+    "rowsep"    : 0.1 * 25.4,
+    "colsep"    : 0.9 * 25.4,
+}
+
+'''
+Servo dimension parameters:
+
+             |<-G->|
+^      =====v===== { H
+E          _I_
+v ________| | |_____
+  ^ |       |<-F->|<> D
+  | |             |
+  B | <--- A ---> |
+  | | (X) C       |
+  v |_____________|
+
+A : motorlength
+B : motorheight
+C : motorwidth
+D : shoulderlength
+
+E : hornheight
+F : hornoffset
+
+G : hornlength
+H : horndepth
+
+        params.setdefault("motorlength")
+        params.setdefault("motorwidth")
+        params.setdefault("motorheight")
+        params.setdefault("shoulderlength", 0)
+
+        params.setdefault("hornheight", 0)
+        params.setdefault("hornoffset", 0)
+
+        params.setdefault("hornlength", 0)
+        params.setdefault("horndepth", 0)
+
+If horn is not symmetric?
+
+        params.setdefault("rhornlength", 0)
+        params.setdefault("lhornlength", 0)
+        if name == "hornlength":
+            self.setParameter("rhornlength", val)
+            self.setParameter("lhornlength", val)
+
+Should horn be a different object?
+
+'''
+
+dims["s4303r"] = { "type" : "servo",
+    "motorlength"   : 31,
+    "motorwidth"    : 17,
+    "motorheight"   : 29,
+    "shoulderlength": 10,
+    "hornlength"    : 38,
+    "hornheight"    : 14,
+    "hornoffset"    : 7,
+    "horndepth"     : 2,
+}
+
+dims["tgy1370a"] = { "type" : "servo",
+    "motorlength"   : 20,
+    "motorwidth"    : 9,
+    "motorheight"   : 14,
+    "shoulderlength": 4,
+    "hornlength"    : 7,
+    "hornheight"    : 10,
+    "hornoffset"    : 4,
+    "horndepth"     : 2,
+}
+
+dims["fs90r"] = { "type" : "servo",
+    "motorlength"   : 23,
+    "motorwidth"    : 12.2,
+    "motorheight"   : 19,
+    "shoulderlength": 5,
+    "hornlength"    : 10,
+    "hornheight"    : 16,
+    "hornoffset"    : 8,
+    "horndepth"     : 2,
+}
+
+l, w, h, r, c, rs, cs = symbols("brainLength brainWidth brainHeight brainNRows brainNCols brainRowSep brainColSep", positive=True)
+
+dims["brainSymbols"] = { "type" : "brains",
+    "length"    : l,
+    "width"     : w,
+    "height"    : h,
+    "nrows"     : r,
+    "ncols"     : c,
+    "rowsep"    : rs,
+    "colsep"    : cs,
+}
+
+l, w, h, s, hl, hh, ho, hd = symbols("servoLength servoWidth servoHeight servoShoulder servoHornLength servoHornHeight servoHornOffset servoHornDepth", positive=True)
+
+dims["servoSymbols"] = { "type" : "servo",
+    "motorlength"   : l,
+    "motorwidth"    : w,
+    "motorheight"   : h,
+    "shoulderlength": s,
+    "hornlength"    : hl,
+    "hornheight"    : hh,
+    "hornoffset"    : ho,
+    "horndepth"     : hd,
+}
diff --git a/rocolib/utils/display.py b/rocolib/utils/display.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cc74c8a4e84ddafc5b5a70463708e6ea6f6bafc
--- /dev/null
+++ b/rocolib/utils/display.py
@@ -0,0 +1,194 @@
+from tkinter import *
+import math
+import numpy
+import logging
+from stl import mesh
+import plotly.graph_objects as go
+
+from rocolib.api.composables.graph.Drawing import *
+from rocolib.api.composables.graph.DrawingEdge import *
+
+
+log = logging.getLogger(__name__)
+
+### copied from https://chart-studio.plotly.com/~empet/15276/converting-a-stl-mesh-to-plotly-gomes/#/
+def stl2mesh3d(stl_mesh):
+    # this function extracts the unique vertices and the lists I, J, K to define a Plotly mesh3d
+    p, q, r = stl_mesh.vectors.shape #(p, 3, 3)
+    # the array stl_mesh.vectors.reshape(p*q, r) can contain multiple copies of the same vertex;
+    # extract unique vertices from all mesh triangles
+    vertices, ixr = numpy.unique(stl_mesh.vectors.reshape(p*q, r), return_inverse=True, axis=0)
+    I = numpy.take(ixr, [3*k for k in range(p)])
+    J = numpy.take(ixr, [3*k+1 for k in range(p)])
+    K = numpy.take(ixr, [3*k+2 for k in range(p)])
+    return vertices, I, J, K
+
+def plotlyFigure(stlmesh, color):
+    vertices, I, J, K = stl2mesh3d(stlmesh)
+    x, y, z = vertices.T
+    colorscale= [[0, color], [1, color]]
+    mesh3D = go.Mesh3d(
+            x=x,
+            y=y,
+            z=z,
+            i=I,
+            j=J,
+            k=K,
+            flatshading=True,
+            colorscale=colorscale,
+            intensity=z,
+            name='model',
+            showscale=False)
+    layout = go.Layout(
+                paper_bgcolor='rgba(0,0,0,0)',
+                plot_bgcolor='rgba(0,0,0,0)',
+                width=1024,
+                height=1024,
+                scene_xaxis_visible=False,
+                scene_yaxis_visible=False,
+                scene_zaxis_visible=False)
+
+    fig = go.Figure(data=[mesh3D], layout=layout)
+    fig.update_layout(margin=dict(r=0, l=0, b=0, t=0),
+                      scene_aspectmode='data', 
+                      width=1024)
+    fig.data[0].update(lighting=dict(ambient= 0.18,
+                                     diffuse= 1,
+                                     fresnel=  .1,
+                                     specular= 0,
+                                     roughness= .1,
+                                     facenormalsepsilon=0))
+    fig.data[0].update(lightposition=dict(x=3000,
+                                          y=3000,
+                                          z=10000));
+
+    return fig
+
+def display3D(fh, ph=None, show=False, color="#ccccff"):
+    ### Modified from https://pypi.org/project/numpy-stl/ docs
+
+    # Load the STL mesh
+    stlmesh = mesh.Mesh.from_file(None, fh=fh)
+    # Back to units of mm
+    stlmesh.vectors *= 1000
+
+    fig = plotlyFigure(stlmesh, color)
+
+    if ph is not None:
+        fig.write_image(ph)
+    if show:
+        fig.update_layout(scene_xaxis_visible=True,
+                          scene_yaxis_visible=True,
+                          scene_zaxis_visible=True)
+        fig.show()
+
+class DisplayApp:
+    def __init__(self, dwg, height = 500, width = 700, showFlats = True):
+        self.root = Tk()
+        self.root.title('Display')
+        self.height = height
+        self.width = width
+        self.canvas = Canvas(self.root, height = self.height, width = self.width)
+        self.canvas.focus_set() #creates the border
+        self.canvas.grid(row =0, column =0, padx = 10, pady = 10)
+        self.dwg = dwg
+
+        self.scale = 1
+        self.showFlats = showFlats
+        #canvas.config(scrollregion = canvas.bbox(ALL))
+
+        self.draw()
+        self.createAddOns()
+        self.bind()
+        self.grid()
+
+        self.pos_x = self.pos_y = 0.0
+
+    def createAddOns(self):
+        self.label = StringVar()
+        self.mode = StringVar()
+        self.coords = StringVar()
+        self.currentc = StringVar()
+        self.label1 = Label(self.root, textvariable = self.label, font = 100, relief = RIDGE, width = 15)
+        self.label2 = Label(self.root,  textvariable = self.mode, font = 100,relief = RIDGE, width = 15)
+        self.label3 = Label(self.root,  textvariable = self.coords , font = 100,relief = RIDGE)
+        self.label4 = Label(self.root, textvariable = self.currentc)
+        self.scrolly = Scrollbar(self.root, command = self.canvas.yview)
+        self.scrollx = Scrollbar(self.root, orient = HORIZONTAL, command = self.canvas.xview)
+
+        self.direction = Canvas(self.root, height = 50, width = 50)
+        self.direction.create_line(0,0,0,0,arrow = LAST, tags = 'direction')
+
+
+    def bind(self):
+        self.canvas.bind('<Motion>', self.current)
+        self.canvas.bind('<Button-1>', self.click)
+        self.canvas.bind('<B1-Motion>', self.drag)
+        self.canvas.bind('<MouseWheel>', self.zoom)
+
+    def grid(self):
+        self.scrolly.grid(row = 0, column = 1, sticky = N + S)
+        self.scrollx.grid(row = 1, column = 0, sticky = E + W)
+
+        self.label1.grid(row = 2, column = 0)
+        self.label2.grid(row = 3, column = 0)
+        self.label3.grid(row = 4, column = 0)
+        self.label4.grid(row = 2, column = 1)
+        self.direction.grid(row = 3, column = 0, sticky = S + E)
+
+        #create_Rectangle = Button(
+
+    def zoom(self, event):
+        if event.delta > 0:
+            self.scale = 1.2
+        elif event.delta < 0:
+            self.scale = .8
+        self.canvas.scale(ALL, self.canvas.canvasx(event.x), self.canvas.canvasy(event.y), self.scale, self.scale)
+        #redraw(canvas, event.x, event.y, img_id = True, k = scale)
+
+
+    def draw(self):
+        print('REDRAWING')
+        k = self.scale
+        dwg = self.dwg
+        print(dwg)
+        color = "white"
+        for e in list(dwg.edges.items()):
+            color = e[1].dispColor(self.showFlats)
+            if color:
+                self.canvas.create_line(k*e[1].x1,k*e[1].y1,k*e[1].x2,k*e[1].y2, fill = color, activewidth = 5, tag = e[0])
+
+
+    def click(self,event):
+        edgename = self.canvas.gettags(event.widget.find_closest(self.canvas.canvasx(event.x),self.canvas.canvasy( event.y)))[0]
+        self.label.set(edgename)
+        self.mode.set(str(self.dwg.edges[edgename].edgetype))
+        self.coords.set(str(self.dwg.edges[edgename].coords()))
+        angle = self.dwg.edges[edgename].angle()
+        self.direction.coords('direction', 25,25,25+25*math.cos(angle),25+25*math.sin(angle))
+        print(edgename, self.dwg.edges[edgename].length())
+
+        self._y = event.y
+        self._x = event.x
+
+
+    def drag(self,event):
+        print("its working")
+        y = (self._y-event.y)
+        if y<0: y *= -1
+        x = (self._x-event.x)
+        if x<0: x *= -1
+
+        self.canvas.yview("scroll",y/self.width,"units")
+        self.canvas.xview("scroll",x/self.height,"units")
+
+        self._x = event.x
+        self._y = event.y
+
+    def current(self,event):
+        c = (self.canvas.canvasx(event.x), self.canvas.canvasy(event.y))
+        self.currentc.set(str(c))
+
+def displayTkinter(dwg, showFlats = True):
+    d = DisplayApp(dwg, showFlats = showFlats)
+    d.root.mainloop()
diff --git a/rocolib/utils/filter.py b/rocolib/utils/filter.py
new file mode 100644
index 0000000000000000000000000000000000000000..bfc90ea8042e31da2791b4f4073aa7720d125f7f
--- /dev/null
+++ b/rocolib/utils/filter.py
@@ -0,0 +1,39 @@
+from rocolib.library import filterComponents
+
+print("~~~")
+print("Actuators")
+print("~~~")
+'''
+print "All:"
+for c in filterComponents(["actuator"]): 
+  print "-", c
+'''
+print("Mechanical actuators:")
+for c in filterComponents(["actuator", "mechanical"]): 
+  print("-", c)
+print("Physical interface devices:")
+for c in filterComponents(["actuator", "device"]): 
+  print("-", c)
+print("Virtual UI widgets:")
+for c in filterComponents(["actuator", "UI"]): 
+  print("-", c)
+
+print()
+
+print("~~~")
+print("Sensors")
+print("~~~")
+'''
+print "All:"
+for c in filterComponents(["sensor"]): 
+  print "-", c
+print "Mechanical feedback sensors:"
+for c in filterComponents(["sensor", "mechanical"]): 
+  print "-", c
+'''
+print("Environmental sensing devices:")
+for c in filterComponents(["sensor", "device"]): 
+  print("-", c)
+print("Virtual UI widgets:")
+for c in filterComponents(["sensor", "UI"]): 
+  print("-", c)
diff --git a/rocolib/utils/numsym.py b/rocolib/utils/numsym.py
new file mode 100644
index 0000000000000000000000000000000000000000..a722b010bc2d52ea0ba610ffccaeb4699fb8c420
--- /dev/null
+++ b/rocolib/utils/numsym.py
@@ -0,0 +1,138 @@
+from functools import reduce
+from operator import add
+import numpy
+import sympy
+
+
+# Common mods
+def list_eye(x):
+    return [[int(i==j) for j in range(x)] for i in range(x)]
+
+def reduce_sum(a):
+  return reduce(add, a)
+
+def cumsum(iterable):
+  arr = [iterable[0]]
+  for c in iterable[1:]:
+    arr.append(arr[-1] + c)
+  return arr
+
+# Numpy mods
+def numpy_rows(x):
+    return x.shape[0]
+  
+def numpy_N(x):
+    return x
+
+def numpy_difference(pts1, pts2):
+    return numpy.linalg.norm(numpy.array(pts1) - numpy.array(pts2))
+
+def numpy_dex(pts1, pts2, tol):
+    return numpy_difference(pts1, pts2) > tol
+
+def numpy_pi():
+    return numpy.pi
+
+def numpy_dotrot(x, rot, angle):
+    return numpy.dot(x, rot(numpy.deg2rad(angle)))
+
+# Sympy mods
+def sympy_deg2rad(x):
+    return x * (sympy.pi / 180)
+
+def sympy_rad2deg(x):
+    return x / (sympy.pi / 180)
+
+def sympy_dot(a, b):
+    return sympy.Matrix(a) * sympy.Matrix(b)
+
+def sympy_norm(x):
+    return sympy.Matrix(x).norm()
+
+def sympy_inv(x):
+    return x.inv()
+
+def sympy_diag(x):
+    return sympy.diag(*x)
+
+def sympy_rows(x):
+    return [x.row(i) for i in range(x.rows)]
+
+def sympy_round(x):
+    return x.round()
+
+def sympy_difference(pts1, pts2):
+    #XXX Hack to overcome precision errors
+    from random import random
+    pts1 = sympy.Matrix(pts1)
+    pts2 = sympy.Matrix(pts2)
+
+    syms = pts1.atoms(sympy.Symbol) | pts2.atoms(sympy.Symbol)
+    subs = [(x, 100 + 100*random()) for x in syms]
+    p1 = numpy.array(pts1.subs(subs)).astype(numpy.float64)
+    p2 = numpy.array(pts2.subs(subs)).astype(numpy.float64)
+    return numpy_difference(p1, p2)
+
+def sympy_dex(pts1, pts2, tol):
+    return sympy_difference(pts1, pts2) > tol
+
+def sympy_rows(x):
+    return x.rows
+
+def sympy_pi():
+    return sympy.pi
+
+def sympy_dotrot(x, rot, angle):
+    return x * rot(sympy_deg2rad(angle))
+
+known_fns = {
+  "cos"       : ( numpy.cos         , sympy.cos         ),
+  "sin"       : ( numpy.sin         , sympy.sin         ),
+  "tan"       : ( numpy.tan         , sympy.tan         ),
+  "sqrt"      : ( numpy.sqrt        , sympy.sqrt        ),
+  "transpose" : ( numpy.transpose   , sympy.transpose   ),
+  "arctan2"   : ( numpy.arctan2     , sympy.atan2       ),
+  "arccos"    : ( numpy.arccos      , sympy.acos        ),
+  "array"     : ( numpy.array       , sympy.Matrix      ),
+  "dot"       : ( numpy.dot         , sympy_dot         ),
+  "norm"      : ( numpy.linalg.norm , sympy_norm        ),
+  "inv"       : ( numpy.linalg.inv  , sympy_inv         ),
+  "diag"      : ( numpy.diag        , sympy_diag        ),
+  "deg2rad"   : ( numpy.deg2rad     , sympy_deg2rad     ),
+  "rad2deg"   : ( numpy.rad2deg     , sympy_rad2deg     ),
+  "round"     : ( numpy.round       , sympy_round       ),
+  "N"         : ( numpy_N           , sympy.N           ),
+  "rows"      : ( numpy_rows        , sympy_rows        ),
+  "difference": ( numpy_difference  , sympy_difference  ),
+  "differenceExceeds": ( numpy_dex  , sympy_dex         ),
+  "pi"        : ( numpy_pi          , sympy_pi          ),
+  "dotrot"    : ( numpy_dotrot      , sympy_dotrot      ),
+  "eye"       : ( list_eye          , list_eye          ),
+  "sum"       : ( reduce_sum        , reduce_sum        ),
+  "cumsum"    : ( cumsum            , cumsum            ),
+}
+
+def __getattr__(fn):
+    if fn not in known_fns:
+        # raise AttributeError(f"{fn} not found in rocolib math library")
+        return getattr(sympy, fn)
+
+    ### TODO: Find a better way of determining whether any of the arguments are sympy expressions?
+    def isSymbolic(a):
+        return "sympy" in repr(type(a))
+    def isSym(args):
+        for a in args:
+            if isSymbolic(a):
+                return True
+            if hasattr(a, '__iter__'):
+                if isSym(a):
+                    return True
+        return False
+
+    def choose(*args, **kwargs):
+        if isSym(args):
+            return known_fns[fn][1](*args, **kwargs)
+        else:
+            return known_fns[fn][0](*args, **kwargs)
+
+    return choose
diff --git a/rocolib/utils/show_connections.py b/rocolib/utils/show_connections.py
new file mode 100644
index 0000000000000000000000000000000000000000..d212cf6c561b3587073357cbd698aa85aa6cd225
--- /dev/null
+++ b/rocolib/utils/show_connections.py
@@ -0,0 +1,27 @@
+from matplotlib import pyplot as plt
+import networkx as nx
+
+from rocolib.api.ports import all_ports
+
+if __name__ == '__main__':
+
+    G = nx.DiGraph()
+
+    labels = {idx: port.__name__ for idx, port in enumerate(all_ports)}
+
+    for idx1, port1 in enumerate(all_ports):
+        for idx2, port2 in enumerate(all_ports):
+            if port1(None).canMate(port2(None)):
+                G.add_edge(idx1, idx2)
+
+    nx.draw(
+        G,
+        pos=nx.spring_layout(G, k=0.5),
+        with_labels=True,
+        labels=labels,
+        font_color='orange',
+        font_weight='bold',
+    )
+
+    plt.show()
+
diff --git a/rocolib/utils/tabs.py b/rocolib/utils/tabs.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f94ddbbc58a919b54c272d3615734024ebd0a4b
--- /dev/null
+++ b/rocolib/utils/tabs.py
@@ -0,0 +1,113 @@
+from rocolib.api.composables.graph.Face import Rectangle
+from rocolib.api.composables.graph.Drawing import Face
+from rocolib.api.composables.graph.DrawingEdge import Edge, Flex
+from rocolib.utils.numsym import pi, arctan2, norm
+from rocolib.utils.utils import prefix
+
+class TabDrawing(Face):
+  def __init__(self, w, t, noflap=False):
+    if w > t:
+      if (noflap or t > w/2 - 1): # HACK what's the right threshold?
+        Face.__init__(self, 
+          ((w,0), (w,t), (0,t)))
+      else:
+        Face.__init__(self, 
+          ((w,0), (w+t,0), (w,t), (0,t), (-t,0)))
+        self.edges['f0'] = Edge("f0", (0,0), (0,t), Flex())
+        self.edges['f1'] = Edge("f1", (w,0), (w,t), Flex())
+      self.transform(origin=(-w/2.0,-t/2.))
+    else:
+      t,w = w,t
+      if (noflap or t > w/2 - 1): # HACK what's the right threshold?
+        Face.__init__(self, 
+          ((0,w), (t,w), (t,0)))
+      else:
+        Face.__init__(self, 
+          ((0,w), (0,w+t), (t,w), (t,0), (0,-t)))
+        self.edges['f0'] = Edge("f0", (0,0), (t,0), Flex())
+        self.edges['f1'] = Edge("f1", (0,w), (t,w), Flex())
+      self.transform(origin=(-t/2.0,-w/2.))
+
+    self.edges.pop('e0')
+
+class SlotDrawing(Face):
+  def __init__(self, w, t, noflap=False):
+    if w > t:
+      Face.__init__(self, ((w+0.5, 0), (w+0.5, 0.5), (0, 0.5)))
+      self.transform(origin=(-w/2. - 0.25, -t/2. - 0.25));
+    else:
+      t,w = w,t
+      Face.__init__(self, ((0, w+0.5), (0.5, w+0.5), (0.5, 0)))
+      self.transform(origin=(-t/2. - 0.25, -w/2. - 0.25));
+
+def BeamTabSlotHelper(face, faceEdge, thick, widget, **kwargs):
+    coords = face.edgeCoords(face.edgeIndex(faceEdge))
+    globalOrigin = coords[0]
+    theta = arctan2(coords[1][1]-coords[0][1], coords[1][0]-coords[0][0])
+    length = norm((coords[1][1]-coords[0][1], coords[1][0]-coords[0][0]))
+
+    try:
+      frac = kwargs['frac']
+    except:
+      frac = 0.5
+    try:
+      noflap = kwargs['noflap']
+    except:
+      noflap = False
+
+    # XXX TODO: Do the same thing with the other aspect ratio
+    n = 0
+    d = length*1.0 / (n*5+1)
+    tw = thick * 3
+    while (tw > thick * 2):
+      n += 1
+      d = length*1.0 / (n*5+1)
+      tw = 2 * d
+
+    t = widget(w=tw, t=thick*frac, noflap=noflap)
+    try:
+      if kwargs["flip"]:
+        t.mirrorX()
+    except: pass
+    t.transform(angle=pi(), origin=(-2 * d, thick/2.))
+    try:
+      if kwargs["mirror"]:
+        t.mirrorY()
+        t.transform(origin=(0, thick))
+    except: pass
+
+    for i in range(n):
+      t.transform(origin=(d * 5, 0))
+      for (name, edge) in t.edges.items():
+        e = edge.copy()
+        e.transform(angle = theta, origin = globalOrigin)
+        face.addDecoration((((e.x1, e.y1), (e.x2, e.y2)), e.edgetype.edgetype))
+      try:
+        if kwargs["alternating"]:
+          t.mirrorY()
+          t.transform(origin=(0, thick))
+      except: pass
+
+def BeamTabDecoration(face, edge, width, **kwargs):
+  return BeamTabSlotHelper(face, edge, width, TabDrawing, **kwargs)
+def BeamSlotDecoration(face, edge, width, **kwargs):
+  return BeamTabSlotHelper(face, edge, width, SlotDrawing, **kwargs)
+
+TABEDGE="tabedge"
+SLOTEDGE="slotedge"
+OPPEDGE="oppedge"
+def BeamTabs(length, width, **kwargs):
+    face = Rectangle('tab', length, width, 
+                        edgeNames=[TABEDGE, "e1", OPPEDGE, "e3"],
+                        recenter=False)
+    BeamTabSlotHelper(face, TABEDGE, width, TabDrawing, **kwargs)
+    face.MAINEDGE = TABEDGE
+    return face
+
+def BeamSlots(length, width, **kwargs):
+    face = Rectangle('slot', length, width, 
+                        edgeNames=[SLOTEDGE, "e1", OPPEDGE, "e3"],
+                        recenter=False)
+    BeamTabSlotHelper(face, SLOTEDGE, width, SlotDrawing, **kwargs)
+    face.MAINEDGE = SLOTEDGE
+    return face
diff --git a/rocolib/utils/transforms.py b/rocolib/utils/transforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..983df2c4a597e8bbcff3fef2e34a1680861b7285
--- /dev/null
+++ b/rocolib/utils/transforms.py
@@ -0,0 +1,96 @@
+from rocolib.utils import numsym as np
+
+
+def MirrorX():
+  return np.diag([-1, 1, 1, 1])
+
+def MirrorY():
+  return np.diag([1, -1, 1, 1])
+
+def MirrorZ():
+  return np.diag([1, 1, -1, 1])
+
+def Scale(scale):
+  return np.diag([scale, scale, scale, 1])
+
+def RotateX(angle):
+  r = np.array([[1, 0, 0, 0],
+                [0, np.cos(angle), -np.sin(angle), 0],
+                [0, np.sin(angle),  np.cos(angle), 0],
+                [0, 0, 0, 1]])
+  return r
+
+def RotateY(angle):
+  r = np.array([[ np.cos(angle), 0, np.sin(angle), 0],
+                [0, 1, 0, 0],
+                [-np.sin(angle), 0, np.cos(angle), 0],
+                [0, 0, 0, 1]])
+  return r
+
+def RotateZ(angle):
+  r = np.array([[np.cos(angle), -np.sin(angle), 0, 0],
+                [np.sin(angle),  np.cos(angle), 0, 0],
+                [0, 0, 1, 0],
+                [0, 0, 0, 1]])
+  return r
+
+def MoveToOrigin(pt):
+  return Translate([-pt[0], -pt[1], 0])
+
+
+def RotateOntoX(pt, pt2=(0,0)):
+  dx = pt[0] - pt2[0]
+  dy = pt[1] - pt2[1]
+  l = np.sqrt(dx * dx + dy * dy)
+  dx = dx / l
+  dy = dy / l
+  r = np.array([[ dx,  dy, 0, 0],
+                [-dy,  dx, 0, 0],
+                [  0,   0, 1, 0],
+                [  0,   0, 0, 1]])
+  return r #RotateZ(-symbolic_atan2(pt[1] - pt2[1], pt[0] - pt2[0]))
+
+
+def MoveOriginTo(pt):
+  return Translate([pt[0], pt[1], 0])
+  
+
+
+def RotateXTo(pt, pt2=(0,0)):
+  dx = pt[0] - pt2[0]
+  dy = pt[1] - pt2[1]
+  l = np.sqrt(dx * dx + dy * dy)
+  dx = dx / l
+  dy = dy / l
+  r = np.array([[ dx, -dy, 0, 0],
+                [ dy,  dx, 0, 0],
+                [  0,   0, 1, 0],
+                [  0,   0, 0, 1]])
+  return r #RotateZ(symbolic_atan2(pt[1] - pt2[1], pt[0] - pt2[0]))
+
+
+def Translate(origin):
+  r = np.array([[1, 0, 0, origin[0]],
+                [0, 1, 0, origin[1]],
+                [0, 0, 1, origin[2]],
+                [0, 0, 0, 1]])
+  return r
+
+def quat2DCM(quat):
+  (a, b, c, d) = quat
+  r = np.array([[a**2 + b**2 - c**2 - d**2, 2*b*c - 2*a*d, 2*b*d + 2*a*c, 0],
+                [2*b*c + 2*a*d, a**2 - b**2 + c**2 - d**2, 2*c*d - 2*a*b, 0],
+                [2*b*d - 2*a*c, 2*c*d + 2*a*b, a**2 - b**2 - c**2 + d**2, 0],
+                [0, 0, 0, 1]])
+  return r
+
+def Transform6DOF(origin, euler=None, quat=None):
+  transform3D = Translate(origin)
+  if euler:
+    transform3D = np.dot(transform3D, RotateZ(euler[2]))
+    transform3D = np.dot(transform3D, RotateY(euler[1]))
+    transform3D = np.dot(transform3D, RotateX(euler[0]))
+  elif quat:
+    transform3D = np.dot(transform3D, quat2DCM(quat))
+  return transform3D
+
diff --git a/rocolib/utils/utils.py b/rocolib/utils/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..20df92137105711e7dbed27aaa1cdf0cd93a5b27
--- /dev/null
+++ b/rocolib/utils/utils.py
@@ -0,0 +1,74 @@
+import rocolib.utils.numsym as np
+from rocolib.utils.transforms import RotateZ, Translate
+
+def prefix(s1, s2):
+  if s1 and s2:
+    return s1 + "." + s2
+  return s1 or s2
+
+def tryImport(module, attribute):
+  try:
+    mod = __import__(module, fromlist=[attribute])
+    obj = getattr(mod, attribute)
+    return obj
+  except ImportError:
+    mod = __import__("rocolib.library." + module, fromlist=[attribute])
+    obj = getattr(mod, attribute)
+    return obj
+
+def decorateGraph(face, decoration, offset=(0, 0), rotate=False, mode=None):
+  try:
+    dfaces = decoration.faces
+  except AttributeError:
+    dfaces = [decoration]
+
+  if mode is None:
+    mode = "hole"
+
+  if rotate is False:
+    rotate = 0
+  elif rotate is True:
+    rotate = -90
+
+  for f in dfaces:
+    t2d = transformDecorations(face, f.pts2d, offset=offset, rotate=rotate, mode=mode)
+    f.transform2D = t2d
+    f.addFace(face, np.inv(t2d))
+
+def transformDecorations(face, pts2d, offset=(0,0), rotate=0, flip=False, mode=None):
+    a = np.deg2rad(rotate)
+    c = np.cos(a)
+    s = np.sin(a)
+
+    face.addDecoration(([
+        (c*p[0] - s*p[1] + offset[0], s*p[0] + c*p[1] + offset[1])
+        for p in pts2d], mode))
+
+    return np.dot(Translate([offset[0], offset[1], 0]), RotateZ(rotate))
+
+def copyDecorations(self, deco_1, deco_2):
+  (ni1, (sc1, i1, p1a, p1b)) = deco_1
+  (ni2, (sc2, i2, p2a, p2b)) = deco_2
+
+  self.inheritInterface(ni1, (sc1, i1))
+  self.inheritInterface(ni2, (sc2, i2))
+
+  f1 = self.getInterface(ni1).getFace()
+  f2 = self.getInterface(ni2).getFace()
+
+  p1o = f1.pts2d[p1a]
+  p1x = f1.pts2d[p1b]
+  p2o = f2.pts2d[p2a]
+  p2x = f2.pts2d[p2b]
+
+  a1 = np.arctan2(p1x[1]-p1o[1], p1x[0]-p1o[0])
+  a2 = np.arctan2(p2x[1]-p2o[1], p2x[0]-p2o[0])
+
+  for pts, mode in f1.decorations:
+      transformDecorations(
+        f2,
+        [(px - p1o[0], py - p1o[1]) for (px, py) in pts],
+        offset = p2o,
+        rotate = np.rad2deg(a2-a1),
+        mode = mode
+      )