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 + )