diff --git a/requirements.txt b/requirements.txt index ca0d4661db6078a4574e9ef07cf2ff0bd657ad17..ece88e046512ba0e6abb71c2ea6520b6e3e212bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ plotly kaleido circlify shapely +dash diff --git a/rocolib/__main__.py b/rocolib/__main__.py index 5df484ccf782b001ef9a3789b642990adb2f0a82..882508ef02ed93b76c2b9bb53f1aa69b3f486751 100644 --- a/rocolib/__main__.py +++ b/rocolib/__main__.py @@ -5,11 +5,12 @@ import sys import logging import argparse from pprint import pprint +from textwrap import shorten from rocolib.library import getComponent, getComponentTree from rocolib.api.composables.graph.Joint import FingerJoint -def test(component, params, thickness, display=False, display3D=False): +def test(component, params, thickness, outdir=None, display=False): f = getComponent(component) if params is not None: for p in params: @@ -21,8 +22,18 @@ def test(component, params, thickness, display=False, display3D=False): t = thickness j = FingerJoint(thickness=t) - f.makeOutput("output/" + component, display=display, display3D=display3D, thickness=t, joint=j) + f.make() + if outdir: + filedir = f"{outdir}/{component}" + ret = f.makeOutput(outputs = True, filedir=filedir, thickness=t, joint=j, remake=False) + + for composable, outs in ret.items(): + print(f"Composable: {composable}") + pprint({k: shorten(str(v), 60) for k, v in outs.items()}) + + if display: + ret = f.visualize(outputs = True, thickness=t, joint=j, remake=False) if __name__ == '__main__': LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] @@ -33,6 +44,7 @@ if __name__ == '__main__': parser.add_argument("component", nargs='?', help="Name of the component you'd like to test") parser.add_argument("--file", "-f", type=str, help="Load component and parameters from file") + parser.add_argument("--output", "-o", type=str, help="Output directory") parser.add_argument("--list", "-l", action='store_true', help="List all known library components") @@ -45,8 +57,7 @@ if __name__ == '__main__': parser.add_argument("-P", help="List component parameters (use multiple times for more detail)", dest="param_list", action="append_const", const=1) parser.add_argument("-t", type=float, help="Thickness (i.e. making with wood)") - parser.add_argument("-d", action='store_true', help="Display 2D drawing") - parser.add_argument("-D", action='store_true', help="Display 3D drawing") + parser.add_argument("-d", action='store_true', help="Display visualizations") parser.add_argument("-p", nargs=2, action='append', metavar=("NAME", "VALUE"), help="Set component parameter NAME to value VALUE") @@ -82,6 +93,14 @@ if __name__ == '__main__': config.update(dict(args.p)) args.p = list(config.items()) + if args.output: + if args.output == '-': + outdir = None + else: + outdir = args.output + else: + outdir = "output" + if args.component: if args.interface_list: f = getComponent(args.component) @@ -91,7 +110,7 @@ if __name__ == '__main__': else: pprint(info) - if not args.p or args.t or args.d or args.D: + if not args.p or args.t or args.d: exit(0) if args.param_list: f = getComponent(args.component) @@ -105,9 +124,9 @@ if __name__ == '__main__': else: pprint(info) - if not args.p or args.t or args.d or args.D: + if not args.p or args.t or args.d: exit(0) - test(args.component, args.p, thickness=args.t, display=args.d, display3D=args.D) + test(args.component, args.p, thickness=args.t, outdir=outdir, display=args.d) acted = True if not acted: diff --git a/rocolib/api/components/Component.py b/rocolib/api/components/Component.py index 8e81033f833af106d8eb5308e3be028197e0c154..b5c2e4ba4e686714aad9726d126563fa3b16eded 100644 --- a/rocolib/api/components/Component.py +++ b/rocolib/api/components/Component.py @@ -7,12 +7,14 @@ import networkx as nx from circlify import circlify from rocolib import ROCOLIB_DIR +from rocolib.api.composables.ComponentComposable import ComponentComposable 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 from rocolib.utils.nx2go import GraphVisualization as gv +from dash import Dash, html log = logging.getLogger(__name__) @@ -35,21 +37,23 @@ def getSubcomponentObject(component, name=None): class Component(Parameterized): @classmethod - def test(cls, params=None, **kwargs): + def test(cls, params=None, outputs=(), filedir=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) + c.makeOutput(outputs, filedir, **kwargs) def __init__(self, yamlFile=None): self._name = None + self.cName = type(self).__name__ Parameterized.__init__(self) self.reset() yf = yamlFile if not yamlFile: yf = type(self).__name__ + ".yaml" + else: + self.cName = yf for fn in (yf, os.path.join(ROCOLIB_DIR, yf), os.path.join(ROCOLIB_LIBRARY, yf)): @@ -60,6 +64,7 @@ class Component(Parameterized): pass if yamlFile: raise ValueError(f"No suitable yamlfile found for {yamlFile}") + self.composables['component'] = ComponentComposable(self) self.predefine() self.define() @@ -380,8 +385,8 @@ class Component(Parameterized): ### Override to combine components' drawings to final drawing pass - def append(self, name, prefix, **kwargs): - component = self.getSubcomponent(name) + def append(self, prefix, **kwargs): + component = self.getSubcomponent(prefix) allPorts = set() for key in component.interfaces: @@ -390,27 +395,40 @@ class Component(Parameterized): 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(): + if key not in self.composables: + self.composables[key] = composable.new() 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(): + for port in allPorts: + port.update(component, composable, self.composables[key], prefix) + + def attach(self, from_interface, to_interface, kwargs): + ifrom = self.getInterfaces(*from_interface) + ito = self.getInterfaces(*to_interface) + + # Interfaces can contain multiple ports, so try each pair of ports + if not isinstance(ifrom, (list, tuple)): + ifrom = [ifrom] + if not isinstance(ito, (list, tuple)): + ito = [ito] + if len(ifrom) != len(ito): + raise AttributeError(f"Number of ports don't match connecting interface {from_interface} to {to_interface}") + + for (fromPort, toPort) in zip(ifrom, ito): try: - composable.attachInterfaces(self.getInterfaces(fromName, fromPort), - self.getInterfaces(toName, toPort), - kwargs) + fromPort.attachTo(toPort, **kwargs) + toPort.attachFrom(fromPort, **kwargs) + + for (key, composable) in self.composables.items(): + composable.attach(fromPort, toPort, **kwargs) except: logstr = "Error in attach: \n" - logstr += f" from ({fromName}, {fromPort}): " - logstr += self.getInterfaces(fromName, fromPort).toString() + logstr += f" from {from_interface}: " + logstr += fromPort.toString() logstr += "\n" - logstr += f" to ({toName}, {toPort}): " - logstr += self.getInterfaces(toName, toPort).toString() + logstr += f" to {to_interface}: " + logstr += toPort.toString() log.error(logstr) raise @@ -478,10 +496,7 @@ class Component(Parameterized): 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) + self.append(name, **kwargs) except: log.error("Error in subclass %s, instance %s" % (classname, name)) raise @@ -496,17 +511,6 @@ class Component(Parameterized): (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: @@ -519,92 +523,52 @@ class Component(Parameterized): 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 makeComponentMap(self, scd=None, full=None, idnum = 0): - if scd is None: - g = self.componentGraph() - full = nx.DiGraph() - full.add_node(0, name="", component=self.getName()) - title = f"<b>{self.getName()}</b>" - else: - g = scd['graph'] - title = f"<b>{scd['component']}</b><br>{scd['name']}" - - data = dict(id=idnum, - datum=(g.graph['depth']+1)**2, - children=[dict(id=len(full), datum=0.5)]) - full.add_node(len(full), title=title) - - if g: - f = nx.convert_node_labels_to_integers(g, len(full)) - full = nx.disjoint_union(full, f) - for sc, scd in f.nodes(data=True): - full, cd = self.makeComponentMap(scd, full, sc) - data["children"].append(cd) - return full, data - - def drawComponentTree(self, basename, stub): - g, data = self.makeComponentMap() - circles = circlify([data]) - - tree = gv(g, - pos = {c.ex['id']: (c.x, c.y) for c in circles}, - node_size = {c.ex['id']: c.r for c in circles}, - node_text = lambda k, v : v.get("title", ""), - node_border_width = lambda k, v: "title" not in v and 2 or 0, - edge_width = 0, - ) - fig = tree.create_figure(width=1024, height=768) - - if basename: - fig.write_image(basename+stub) - ret = basename+stub + def visualize(self, outputs=True, **ka): + widgets = self.makeOutput(outputs, widgets=True, **ka) + app = Dash(__name__) + elts = [ + html.H2('RoCo component visualizer'), + html.H1(f"Component: {self.getName()}"), + ] + for c, w in widgets.items(): + elts.append(html.H3(f"Composable: {c}")) + for k, v in w.items(): + elts.append(html.H4(f"Widget: {k}")) + elts.append(html.P(v['desc'])) + elts.append(v['widget']) + app.layout = html.Div(elts) + app.run_server(debug=True) + + def makeOutput(self, outputs=(), filedir=None, widgets=False, **ka): + if filedir: + log.info(f"Compiling robot designs to directory {filedir} ...") + elif widgets: + log.info(f"Generating visualization ...") else: - ret = fig.to_image(format="png") - return ret - - 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) + log.info(f"Compiling robot design dictionary ...") - if kw("remake", True): - self.make(kw("useDefaultParameters", True)) + if ka.pop("remake", True): + self.make(ka.pop("useDefaultParameters", True)) log.debug(f"... done making {self.getName()}.") # XXX: Is this the right way to do it? import os try: - os.makedirs(filedir) + filedir and 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.drawComponentTree(filedir, "/tree.png") - log.debug("done making tree.") + + for (name, composable) in self.composables.items(): + ss = composable.makeOutput(name, outputs, filedir, widgets, **ka) + rets[name] = ss log.info("Happy roboting!") return rets diff --git a/rocolib/api/components/FoldedComponent.py b/rocolib/api/components/FoldedComponent.py index e41f3401c45eab3d1e4e229aedd67b146696951f..886382b57a4dabf967c3df99cb140a21dca30c92 100644 --- a/rocolib/api/components/FoldedComponent.py +++ b/rocolib/api/components/FoldedComponent.py @@ -33,7 +33,7 @@ class FoldedComponent(MechanicalComponent): self.setFaceInterface(interface, face) def setEdgeInterface(self, interface, edges, lengths): - self.setInterface(interface, EdgePort(self, edges, lengths)) + self.setInterface(interface, EdgePort(self, self.getGraph(), edges, lengths)) def setFaceInterface(self, interface, face): self.setInterface(interface, FacePort(self, self.getGraph(), face)) diff --git a/rocolib/api/composables/ComponentComposable.py b/rocolib/api/composables/ComponentComposable.py new file mode 100644 index 0000000000000000000000000000000000000000..8f1523247bd933dc551a19b6e0107850f4d15e17 --- /dev/null +++ b/rocolib/api/composables/ComponentComposable.py @@ -0,0 +1,74 @@ +from rocolib.api.composables.Composable import Composable, output, widget +import networkx as nx +from circlify import circlify +from rocolib.utils.nx2go import GraphVisualization as gv +from dash import dcc + + +class ComponentComposable(Composable): + def __init__(self, component): + self.c = component + self.net = nx.DiGraph() + self.net.graph['depth'] = 0 + + def append(self, c2, name, **kwargs): + sc = c2.c + scname = sc.getName() + scclass = sc.cName + scnet = c2.net + + self.net.add_node(scname, name=name, component=scclass, net=scnet) + self.net.graph['depth'] = max(self.net.graph['depth'], scnet.graph['depth']+1) + + def attach(self, fromPort, toPort, **kwargs): + self.net.add_edge(fromPort.parent.getName(), toPort.parent.getName()) + + def _makeComponentMap(self, scd=None, full=None, idnum = 0): + if scd is None: + g = self.net + full = nx.DiGraph() + full.add_node(0, name="", component=self.c.getName()) + title = f"<b>{self.c.getName()}</b>" + else: + try: + g = scd['net'] + except KeyError: + print(scd, idnum) + raise + title = f"<b>{scd['component']}</b><br>{scd['name']}" + + data = dict(id=idnum, + datum=(g.graph['depth']+1)**2, + children=[dict(id=len(full), datum=0.5)]) + full.add_node(len(full), title=title) + + if g: + f = nx.convert_node_labels_to_integers(g, len(full)) + full = nx.disjoint_union(full, f) + for sc, scd in f.nodes(data=True): + full, cd = self._makeComponentMap(scd, full, sc) + data["children"].append(cd) + return full, data + + def _drawComponentTree(self): + g, data = self._makeComponentMap() + circles = circlify([data]) + + tree = gv(g, + pos = {c.ex['id']: (c.x, c.y) for c in circles}, + node_size = {c.ex['id']: c.r for c in circles}, + node_text = lambda k, v : v.get("title", ""), + node_border_width = lambda k, v: "title" not in v and 2 or 0, + edge_width = 0, + ) + fig = tree.create_figure(width=1024, height=768) + return fig + + @output("map.png", "component topology map", binary=True) + def drawComponentMap(self, fp, **ka): + fig = self._drawComponentTree() + fig.write_image(fp, format="png") + + @widget('displaymap', "Show component topology") + def displayMap(self, fp, **ka): + return dcc.Graph(figure = self._drawComponentTree()) diff --git a/rocolib/api/composables/Composable.py b/rocolib/api/composables/Composable.py index eaccb69277cd6d4e0f49294e355c63c8ee719dbc..8b51fec41e414970542ae286d74fb4fb7953dbaf 100644 --- a/rocolib/api/composables/Composable.py +++ b/rocolib/api/composables/Composable.py @@ -1,30 +1,66 @@ +import logging +from io import BytesIO, StringIO + + +log = logging.getLogger(__name__) + +def output(stub, desc, binary=False, widget=False, **kwargs): + def wrap(f): + def inner(self, fp, **ka): + ka.update(kwargs) + return f(self, fp, **ka) + inner.output = dict(f=inner, desc=desc, stub=stub, binary=binary, widget=widget) + return inner + return wrap + +def widget(stub, desc, **kwargs): + return output(stub, desc, widget=True, **kwargs) + 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): - fromPort.attachTo(toPort, self, **kwargs) - toPort.attachFrom(fromPort, self, **kwargs) - - - - def makeOutput(self, filedir, **kwargs): - raise NotImplementedError + def new(self): + return self.__class__() + def append(self, newComposable, newPrefix, **kwargs): + pass + def attach(self, fromPort, toPort, **kwargs): + pass + + @classmethod + def listOutputs(cls, widgets=False): + outputs = {} + for methodname in dir(cls): + method = getattr(cls, methodname) + if hasattr(method, 'output'): + out = method.output + keyword = out['stub'].split('.')[0] + if out['widget'] == widgets: + outputs[keyword] = out + return outputs + + def makeOutput(self, name, outputs=(), filedir=None, widgets=False, **ka): + basename = None + if filedir: + basename = filedir + f"/{name}-" + + knownouts = self.listOutputs(widgets) + + rets = {} + if outputs is True: + outputs = knownouts + + for k in outputs: + if k in knownouts: + v = knownouts[k] + log.info("Generating %s ..." % v['desc']) + if basename and v['stub']: + fn = basename+v['stub'] + fp = open(fn, v['binary'] and 'wb' or 'w') + rets[k] = fn + else: + fp = v['binary'] and BytesIO() or StringIO() + ret = v['f'](self, fp, **ka) + if widgets: + rets[k] = dict(desc=v['desc'], widget=ret) + if k not in rets: + rets[k] = fp.getvalue() + fp.close() + return rets diff --git a/rocolib/api/composables/GraphComposable.py b/rocolib/api/composables/GraphComposable.py index 16dd2614d4e4e2ebfa46e38118ed639ba28ce83d..d056063fea22613899c557266cff902a39fdb8a8 100644 --- a/rocolib/api/composables/GraphComposable.py +++ b/rocolib/api/composables/GraphComposable.py @@ -1,133 +1,83 @@ -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.api.composables.Composable import Composable, output, widget from rocolib.utils.tabs import BeamTabs, BeamTabDecoration, BeamSlotDecoration -import rocolib.utils.numsym as np - +from rocolib.utils.display import meshfig +from dash import dcc -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 + def __init__(self): + BaseGraph.__init__(self) class GraphComposable(Composable, BaseGraph): - def __init__(self): - BaseGraph.__init__(self) - - def append(self, g2, prefix2, **kwargs): - if kwargs.get("mirror", False): - g2.mirror() - 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 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", "stl", self.to3D, "model.stl", format="stl", **kwargs) - handle("webots", "webots", self.to3D, "model.wbo", format="webots", **kwargs) - - - if kw("display3D", False) or kw("png"): - from rocolib.utils.display import display3D - - if basename: - if kw("stl"): - try: - stl_handle = open(basename+"model.stl", 'rb') - except: - log.info("No Stl found") - return + def __init__(self): + BaseGraph.__init__(self) + self._d = None + + @property + def d(self): + if self._d is None: + self._d = Drawing(self) + self._d.transform(relative=(0,0)) + return self._d + + def append(self, g2, prefix2, **kwargs): + if kwargs.get("mirror", False): + g2.mirror() + 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: - log.info("STL wasn't selected, making now...") - handle("png", "stl", self.to3D, None, format="stl") - 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() + self.faces.extend(g2.faces) + self.edges.extend(g2.edges) - with stl_handle as sh: - display3D(sh, png_file, kw("display3D", False)) + def makeOutput(self, name, outputs, filedir=None, widgets=False, **ka): + self.tabify(ka.pop("tabFace", BeamTabs), ka.pop("tabDecoration", BeamTabDecoration), + ka.pop("slotFace", None), ka.pop("slotDecoration", BeamSlotDecoration), **ka) + if ka.pop("joint", None): + self.jointify(**ka) + self.place(transform3D=ka.pop("transform3D", None)) - if kw("png") and not basename: - rets["png"] = png_file.getvalue() + return Composable.makeOutput(self, name, outputs, filedir, widgets, **ka) - if kw("display", False): - from rocolib.utils.display import displayTkinter - displayTkinter(d) - - return rets + def to2D(self, fp, **ka): + if ka.get("mode", "") in "Corel print animate".split(): + return self.d.toSVG(fp, mode=ka.get("mode")) + else: + return self.d.toDXF(fp, mode=ka.get("mode", "silhouette")) + + def to3D(self, fp, **ka): + if ka.get('format') == "stl": + self.fullmesh.save("roco-model", fh=fp) + elif ka.get('format') == "png": + meshfig(self.fullmesh, axes=False).write_image(fp) + elif ka.get('format') == 'webots': + from rocolib.utils.roco2sim.wbo_nodes import wboConverter + fp.write(wboConverter(self.meshes)) + + corelOut = output("corel.svg", "Corel cut-and-fold pattern", mode="Corel") (to2D) + printOut = output("printer.svg", "cut pattern for printing", mode="print") (to2D) + animOut = output("animate.svg", "input for OrigamiSimulator", mode="animate") (to2D) + cutOut = output("silhouette.dxf", "Silhouette cut-and-fold pattern", mode="silhouette")(to2D) + autoOut = output("autofold.dxf", "autofolding pattern", mode="autofold") (to2D) + + stlOut = output("model.stl", "3D STL model", binary=True, format="stl") (to3D) + pngOut = output("render.png", "3D render", binary=True, format="png") (to3D) + wboOut = output("webots.wbo", "Webots robot", format="webots") (to3D) + + @widget('display3D', "Show 3D model") + def display3D(self, fp, **ka): + kwargs = {k:v for k,v in ka.items() if k in "color scale".split()} + return dcc.Graph(figure = meshfig(self.fullmesh, **kwargs)) + + def display2D(self): + if kw("display", False): + from rocolib.utils.display import displayTkinter + displayTkinter(d) + + return rets diff --git a/rocolib/api/composables/graph/Drawing.py b/rocolib/api/composables/graph/Drawing.py index 44cdabe907632a6a550b84a0a61998c88f43a8fa..7c5e9ea068701609a3e94358305a3ddc2a3b8063 100644 --- a/rocolib/api/composables/graph/Drawing.py +++ b/rocolib/api/composables/graph/Drawing.py @@ -8,13 +8,15 @@ from rocolib.utils.utils import prefix as prefixString log = logging.getLogger(__name__) class Drawing: - def __init__(self): + def __init__(self, g=None): """ Initializes an empty dictionary to contain Edge instances. Keys will be Edge labels as strings. Key values will be Edge instances. """ self.edges = {} + if g: + self.fromGraph(g) def fromGraph(self, g): maxx = 0 @@ -143,18 +145,6 @@ class Drawing: 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 @@ -178,33 +168,6 @@ class Drawing: 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() @@ -271,13 +234,6 @@ class Drawing: 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) diff --git a/rocolib/api/composables/graph/Face.py b/rocolib/api/composables/graph/Face.py index aaaad3650d54b5b5c2d61e34755b9b95503ca918..9be77bb764f586e3d41468dd1833095a0572f0d6 100644 --- a/rocolib/api/composables/graph/Face.py +++ b/rocolib/api/composables/graph/Face.py @@ -272,10 +272,10 @@ class Face(object): el = self.edgeLength(i) try: if el <= 0.01: - log.info(f'Skipping traversal of short edge, length = {el}') + log.debug(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}') + log.debug(f'Sympyicized variable detected - ignoring edge length check on edge {e.name}') da = e.faces[self] diff --git a/rocolib/api/composables/graph/Graph.py b/rocolib/api/composables/graph/Graph.py index 17f4dc40aa3bd9b5d2446d4a2d601cb3a63866ac..b8217b84a1009bf845d608c496c82d8650a5ef5c 100644 --- a/rocolib/api/composables/graph/Graph.py +++ b/rocolib/api/composables/graph/Graph.py @@ -1,49 +1,45 @@ import logging +from stl import mesh, Mode from sect.triangulation import Triangulation from ground.base import get_context from rocolib.api.composables.graph.HyperEdge import HyperEdge from rocolib.utils.utils import prefix as prefixString -from rocolib.utils.roco2sim.Node import ShapeNode import rocolib.utils.numsym as np -from rocolib.utils.roco2sim.format_3d import format_wrl 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 + dt = np.array([[0],[0],[thickness/2.],[0]]) + nf = face-dt + pf = face+dt - faces = [] + 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 + 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 + return faces def _triangulate(faces, **kwargs): """Create triangulated faces in 3D space with thickness""" scale = .001 # roco units : mm ; STL units m - triangles_coord_index = [] - triangles_facet_normal = [] - facets_index = [] - k = 0 # index initiated + tris = [] thickness = kwargs.get("thickness", 0) nparr = lambda l: np.transpose([list(x) + [0, 1] for x in l]) for f in faces: if f.area == 0: - log.info(f"Omitting face {f.name} with area {f.area} from solid model") + log.debug(f"Omitting face {f.name} with area {f.area} from solid model") continue # skip faces with 0 area r = f.transform3D facets_coord_index = [] @@ -51,7 +47,6 @@ def _triangulate(faces, **kwargs): facets = [] def add_triangles(t, edges=False): - nonlocal k if thickness: facets = inflate(t, thickness=thickness, edges=edges) elif edges: @@ -60,10 +55,9 @@ def _triangulate(faces, **kwargs): facets = [t] for x in facets: - facets_coord_index.extend(np.transpose(np.dot(r, x) * scale)) - facets_normal.extend(np.transpose(np.dot(r, x) * scale)) - facets_index.append([k, k + 1, k + 2]) - k = k + 3 + tri = np.transpose(np.dot(r, x)[0:3] * scale).tolist() + tris.append(tri) + poly = f.polygon() triangles = ( x.vertices for x in Triangulation.constrained_delaunay(poly, context=get_context()).triangles() ) @@ -72,306 +66,271 @@ def _triangulate(faces, **kwargs): add_triangles(t) for t in map(nparr, poly.edges): add_triangles(t, edges=True) - triangles_coord_index.extend(facets_coord_index) - triangles_facet_normal.extend(facets_normal) - return format_wrl(triangles_coord_index, triangles_facet_normal, facets_index) + data = np.zeros(len(tris), dtype=mesh.Mesh.dtype) + solid = mesh.Mesh(data) + for i, tri in enumerate(tris): + for j in range(3): + solid.vectors[i][j] = tri[j] + return solid class Graph(): - def __init__(self): - self.faces = [] - self.facelists = [] - self.edges = [] - self.shapeNodes=None - - 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) + def __init__(self): + self.faces = [] + self.facelists = [] + self.edges = [] + self._meshes = [] + self._fullmesh = None + + @property + def meshes(self): + if not self._meshes: + self.makeMeshes() + return self._meshes + + @property + def fullmesh(self): + if not self._fullmesh: + self.makeMeshes() + return self._fullmesh + + 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 - 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): - raise NotImplementedError - for f in self.faces: - f.flip() - - def transform(self, scale=1, angle=0, origin=(0,0)): - raise NotImplementedError - - def dotransform(self, scale=1, angle=0, origin=(0,0)): - for f in self.faces: - f.transform(scale, angle, origin) - - def mirror(self): - for f in self.faces: - f.mirror() - - def mirrorY(self): - raise NotImplementedError - for f in self.faces: - f.transform( mirrorY()) - - def mirrorX(self): - raise NotImplementedError - 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 = [] + 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 - if transform3D is None: - transform3D = np.eye(4) + return self - while True: + def getFace(self, name): 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 + 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: - break - - #IPython.embed() - self.rebuildEdges() - - def unplace(self): - - for f in self.faces: - f.transform2D = None - f.transform3D = None + e2.mergeWith(e1, angle=angle, flip=True, tabWidth=tabWidth) + self.edges.remove(e1) - for e in self.edges: - e.pts2D = None - e.pts3D = None - - def toShapeNodes(self, **kwargs): - """Create a shape node list""" - self.place() - self.shapeNodes = [] - for i, faces in enumerate(self.facelists): - # Create 1 shape node per set of faces. - wrl = _triangulate(faces, **kwargs) - self.shapeNodes.append(ShapeNode(f"solid{i}", wrl, faces[0].transform3D)) - - def to3D(self, fp, **kwargs): - """stl or webots expects fp instance from handle. if None, not creating it""" - if self.shapeNodes is None: - self.toShapeNodes(**kwargs) - - if kwargs.get('format') == 'stl': - from .stlwriter import ASCII_STL_Writer as STL_Writer - writer = STL_Writer(fp) - for sn in self.shapeNodes: - writer.add_faces(sn.getSTL().face) - writer.close() - - if kwargs.get('format') == 'webots': - from rocolib.utils.roco2sim.wbo_nodes import wboConverter - fp.write(wboConverter(self.shapeNodes)) - - - ''' - @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 - ''' + 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): + raise NotImplementedError + for f in self.faces: + f.flip() + + def transform(self, scale=1, angle=0, origin=(0,0)): + raise NotImplementedError + + def dotransform(self, scale=1, angle=0, origin=(0,0)): + for f in self.faces: + f.transform(scale, angle, origin) + + def mirror(self): + for f in self.faces: + f.mirror() + + def mirrorY(self): + raise NotImplementedError + for f in self.faces: + f.transform( mirrorY()) + + def mirrorX(self): + raise NotImplementedError + 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 makeMeshes(self, **kwargs): + """Create a list of stl Mesh objects""" + self.place() + # Create 1 mesh per connected set of faces. + self._meshes = [_triangulate(faces, **kwargs) for faces in self.facelists] + self._fullmesh = mesh.Mesh(np.concatenate([m.data for m in self._meshes])) diff --git a/rocolib/api/composables/graph/stlwriter.py b/rocolib/api/composables/graph/stlwriter.py deleted file mode 100644 index 96b82b779bd8106e327c307fcb1478f1d29b5105..0000000000000000000000000000000000000000 --- a/rocolib/api/composables/graph/stlwriter.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/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/DecorationPort.py b/rocolib/api/ports/DecorationPort.py index aa3f2b236e3f3404f0fcffd2b35f79ea0ace21d2..1707f2d9a5150e1c8cf29744a0c5d82dfd68b434 100644 --- a/rocolib/api/ports/DecorationPort.py +++ b/rocolib/api/ports/DecorationPort.py @@ -17,10 +17,10 @@ class DecorationPort(Port): def canMate(self, otherPort): return (otherPort.getFaceName() is not None) - def attachFrom(self, fromPort, graph, **kwargs): + def attachFrom(self, fromPort, **kwargs): # If from face to decoration, we can decorate the face if isinstance(fromPort, FacePort): - face = graph.getFace(fromPort.getFaceName()) + face = fromPort.getFace() deco = self.getDecoration() if face is not None: decorateGraph(face, decoration=deco, **kwargs) @@ -37,8 +37,8 @@ class AnchorPort(FacePort): def canMate(self, otherPort): return False - def attachFrom(self, fromPort, graph, **kwargs): + def attachFrom(self, fromPort, **kwargs): if isinstance(fromPort, DecorationPort): deco = fromPort.getDecoration().faces[0] face = self.getFaceName() - graph.mergeFace(deco.joinedFaces[0][0].name, face, np.dot(self.getTransform(), deco.transform2D)) + self.graph.mergeFace(deco.joinedFaces[0][0].name, face, np.dot(self.getTransform(), deco.transform2D)) diff --git a/rocolib/api/ports/EdgePort.py b/rocolib/api/ports/EdgePort.py index 60c2c44a198b47b23ee1cc78fc1ebb33a8a5e0cd..b4babc965cb6aa94912d080e62d632e3808072e2 100644 --- a/rocolib/api/ports/EdgePort.py +++ b/rocolib/api/ports/EdgePort.py @@ -1,54 +1,56 @@ 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()) - - - def attachTo(self, toPort, graph, **kwargs): - # if connecting from edge to edge, merge them in order - if isinstance(toPort, EdgePort): - label1 = self.getEdges() - label2 = toPort.getEdges() - # 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 graph.edges): - return - if label2[i] not in (e.name for e in graph.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 - graph.mergeEdge(label1[i], label2[i], **newargs) + def __init__(self, parent, graph, 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.graph = graph + self.edges = edges + self.lengths = lengths + + def getEdges(self): + return self.edges + + def getParams(self): + return self.lengths + + def update(self, newparent, oldcomposable, newcomposable, prefix): + self.setParent(newparent) + if oldcomposable == self.graph: + self.edges = [prefixString(prefix, e) for e in self.edges] + self.graph = newcomposable + + def canMate(self, otherPort): + try: + return len(self.getEdges()) == len(otherPort.getEdges()) + except AttributeError: + return False + + def toString(self): + return str(self.getEdges()) + + + def attachTo(self, toPort, **kwargs): + if isinstance(toPort, EdgePort): + label1 = self.getEdges() + label2 = toPort.getEdges() + + 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 + try: + self.graph.mergeEdge(label1[i], label2[i], **newargs) + except AttributeError: + # Edge not found, skipping + pass diff --git a/rocolib/api/ports/FacePort.py b/rocolib/api/ports/FacePort.py index 3b3a162605a22763c9b63a5f931a7cf4ea380337..808c68c0937af140688ad4b319f008a39d526a57 100644 --- a/rocolib/api/ports/FacePort.py +++ b/rocolib/api/ports/FacePort.py @@ -7,8 +7,11 @@ class FacePort(Port): self.graph = graph self.face = face - def prefix(self, prefix=""): - self.face = prefixString(prefix, self.face) + def update(self, newparent, oldcomposable, newcomposable, prefix): + self.setParent(newparent) + if oldcomposable == self.graph: + self.face = prefixString(prefix, self.face) + self.graph = newcomposable def getFace(self): return self.graph.getFace(self.face) @@ -31,9 +34,9 @@ class FacePort(Port): except: pass - def attachTo(self, toPort, graph, **kwargs): + def attachTo(self, toPort, **kwargs): # if connecting from face to face, merge them in order if isinstance(toPort, FacePort): - face1 = graph.getFace(self.getFaceName()) - face2 = graph.getFace(toPort.getFaceName()) - graph.mergeFace(face1.name, face2.name) + face1 = self.graph.getFace(self.getFaceName()) + face2 = self.graph.getFace(toPort.getFaceName()) + self.graph.mergeFace(face1.name, face2.name) diff --git a/rocolib/api/ports/Port.py b/rocolib/api/ports/Port.py index 838bd34af9025c7b0033d920935fe338208dfd48..d8660791fa6d6906bb7c4b3279c9edb10be0ee60 100644 --- a/rocolib/api/ports/Port.py +++ b/rocolib/api/ports/Port.py @@ -31,9 +31,9 @@ class Port(Parameterized): # Override to handle getting inherited to parent.interfaces[name] pass - def prefix(self, prefix=""): - # Override to handle prefixing - pass + def update(self, newparent, oldcomposable, newcomposable, prefix): + # Override to handle what happens when a composable gets appended to another with a prefix + self.setParent(newparent) def canMate(self, otherPort): @@ -68,10 +68,10 @@ class Port(Parameterized): def toString(self): return str(self.parent) + '.' + self.getName() - def attachTo(self, toPort, composable, **kwargs): + def attachTo(self, toPort, **kwargs): pass - def attachFrom(self, fromPort, composable, **kwargs): + def attachFrom(self, fromPort, **kwargs): pass diff --git a/rocolib/test/test_library.py b/rocolib/test/test_library.py index 4fe21d84cd1dfcf370ce7aa291bc443537030f95..61b6379da60768fd5ff1237d8247e932566c08f0 100644 --- a/rocolib/test/test_library.py +++ b/rocolib/test/test_library.py @@ -15,7 +15,7 @@ def test_component_python(component): @pytest.mark.parametrize("component",yamlComponents) def test_component_yaml(component): - getComponent(component).makeOutput(default=False) + getComponent(component).makeOutput() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) diff --git a/rocolib/utils/display.py b/rocolib/utils/display.py index 5cc74c8a4e84ddafc5b5a70463708e6ea6f6bafc..35b27834eed270d38048221640674f9fcc72fb2a 100644 --- a/rocolib/utils/display.py +++ b/rocolib/utils/display.py @@ -2,7 +2,6 @@ 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 * @@ -11,42 +10,35 @@ 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): +### adapted from https://chart-studio.plotly.com/~empet/15276/converting-a-stl-mesh-to-plotly-gomes/#/ +def stl2mesh3d(stl_mesh, scale=1000): + # scale = 1000 back into roco units = mm + # 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 + 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)]) + x, y, z = scale * vertices.T + return dict(x=x, y=y, z=z, i=i, j=j, k=k) + +def meshfig(stlmesh, scale=1000, color="#ccccff", axes=True): colorscale= [[0, color], [1, color]] mesh3D = go.Mesh3d( - x=x, - y=y, - z=z, - i=I, - j=J, - k=K, + **stl2mesh3d(stlmesh, scale), 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) + scene_xaxis_visible=axes, + scene_yaxis_visible=axes, + scene_zaxis_visible=axes) fig = go.Figure(data=[mesh3D], layout=layout) fig.update_layout(margin=dict(r=0, l=0, b=0, t=0), @@ -64,24 +56,6 @@ def plotlyFigure(stlmesh, color): 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() diff --git a/rocolib/utils/numsym.py b/rocolib/utils/numsym.py index 42855a4a31921d07795e872ecc2257441e1aa251..920417cdcbf13d939c473f07882bb8f0421fdac5 100644 --- a/rocolib/utils/numsym.py +++ b/rocolib/utils/numsym.py @@ -100,6 +100,8 @@ known_fns = { "arctan2" : ( numpy.arctan2 , sympy.atan2 ), "arccos" : ( numpy.arccos , sympy.acos ), "array" : ( numpy.array , sympy.Matrix ), + "zeros" : ( numpy.zeros , sympy.zeros ), + "concatenate": ( numpy.concatenate, sympy.BlockMatrix ), "dot" : ( numpy.dot , sympy_dot ), "norm" : ( numpy.linalg.norm , sympy_norm ), "inv" : ( numpy.linalg.inv , sympy_inv ), diff --git a/rocolib/utils/roco2sim/wbo_nodes.py b/rocolib/utils/roco2sim/wbo_nodes.py index ebc6a02517add235cc39f85601a4ab19847c232f..17b99cbe2ea522980be83bd1cc8c1033a92d6651 100644 --- a/rocolib/utils/roco2sim/wbo_nodes.py +++ b/rocolib/utils/roco2sim/wbo_nodes.py @@ -1,3 +1,4 @@ +from stl import mesh from typing import List from rocolib.utils.roco2sim.Node import * @@ -120,10 +121,28 @@ def wboHeader(version="R2021b"): """ -def wboConverter(sNodes: List[ShapeNode]): - robot = sNodes[0] - for sNode in sNodes[1:len(sNodes)]: - robot.children.append(wboSolid(sNode)) +def mesh2wrl(m: mesh.Mesh): + m.update_normals() + coords = set() + for v in m.vectors: + coords.update(map(tuple, v)) + coords = list(coords) + vertices = [] + for v in m.vectors: + vertices.append([coords.index(tuple(x)) for x in v]) + coords = list(map(np.array, coords)) + return wrl(coords, m.normals, vertices) + + +def mesh2sNode(i, m: mesh.Mesh): + wrl = mesh2wrl(m) + return ShapeNode(f"solid{i}", wrl, np.eye(3)) + + +def wboConverter(meshes: List[mesh.Mesh]): + robot = mesh2sNode(0,meshes[0]) + for i in range(1,len(meshes)): + robot.children.append(wboSolid(mesh2sNode(i, meshes[i]))) node = wboHeader() + wboRobot(robot) return node