diff --git a/rocolib/__main__.py b/rocolib/__main__.py index 882508ef02ed93b76c2b9bb53f1aa69b3f486751..d00adae95801fad51b7bfce019df138935f3d0ab 100644 --- a/rocolib/__main__.py +++ b/rocolib/__main__.py @@ -6,6 +6,7 @@ import logging import argparse from pprint import pprint from textwrap import shorten +from dash import Dash from rocolib.library import getComponent, getComponentTree from rocolib.api.composables.graph.Joint import FingerJoint @@ -33,7 +34,10 @@ def test(component, params, thickness, outdir=None, display=False): 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) + app = Dash(__name__) + html = f.visualize(outputs = True, thickness=t, joint=j, remake=False) + app.layout = html + app.run_server(debug=True) if __name__ == '__main__': LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] diff --git a/rocolib/api/components/Component.py b/rocolib/api/components/Component.py index b5c2e4ba4e686714aad9726d126563fa3b16eded..d5ca60d6bad3567aadf2468f8749731ceb8d1091 100644 --- a/rocolib/api/components/Component.py +++ b/rocolib/api/components/Component.py @@ -14,7 +14,7 @@ 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 +from dash import html, dcc log = logging.getLogger(__name__) @@ -531,19 +531,23 @@ class Component(Parameterized): 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()}"), - ] + elts = [ html.H1(f"RoCo component visualizer: {self.getName()}") ] + tabs = [] + for c, w in widgets.items(): - elts.append(html.H3(f"Composable: {c}")) + widgets = [] 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) + widgets.append(html.Div([ + html.H3(v['desc']), + html.P(f"Widget: {k}"), + v['widget'], + ], style={'padding': 10, 'flex': 1})) + tabs.append(dcc.Tab(label = f"Composable: {c}", + children = html.Div(widgets, style={'display': 'flex', 'flex-direction': 'row'}) + )) + + elts.append(html.Div(dcc.Tabs(tabs))) + return html.Div(elts) def makeOutput(self, outputs=(), filedir=None, widgets=False, **ka): if filedir: diff --git a/rocolib/api/composables/ComponentComposable.py b/rocolib/api/composables/ComponentComposable.py index 8f1523247bd933dc551a19b6e0107850f4d15e17..48c3fb19b113d37aaac9c1fc23f1bd429a95481b 100644 --- a/rocolib/api/composables/ComponentComposable.py +++ b/rocolib/api/composables/ComponentComposable.py @@ -69,6 +69,6 @@ class ComponentComposable(Composable): fig = self._drawComponentTree() fig.write_image(fp, format="png") - @widget('displaymap', "Show component topology") + @widget('displaymap', "component topology map") def displayMap(self, fp, **ka): return dcc.Graph(figure = self._drawComponentTree()) diff --git a/rocolib/api/composables/GraphComposable.py b/rocolib/api/composables/GraphComposable.py index d056063fea22613899c557266cff902a39fdb8a8..502f76423822650a6de5c6ea0ddbdcf59696fe4b 100644 --- a/rocolib/api/composables/GraphComposable.py +++ b/rocolib/api/composables/GraphComposable.py @@ -2,7 +2,7 @@ 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, output, widget from rocolib.utils.tabs import BeamTabs, BeamTabDecoration, BeamSlotDecoration -from rocolib.utils.display import meshfig +from rocolib.utils.display import meshfig, cutfig from dash import dcc @@ -37,6 +37,7 @@ class GraphComposable(Composable, BaseGraph): self.edges.extend(g2.edges) def makeOutput(self, name, outputs, filedir=None, widgets=False, **ka): + self._thickness = ka.get("thickness", 0) self.tabify(ka.pop("tabFace", BeamTabs), ka.pop("tabDecoration", BeamTabDecoration), ka.pop("slotFace", None), ka.pop("slotDecoration", BeamSlotDecoration), **ka) if ka.pop("joint", None): @@ -70,14 +71,12 @@ class GraphComposable(Composable, BaseGraph): 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") + @widget('display3D', "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 + @widget('display2D', "2D cut pattern") + def display2D(self, fp, **ka): + kwargs = {k:v for k,v in ka.items() if k in "scale axes".split()} + return dcc.Graph(figure = cutfig(self.getEdgeSet(), **kwargs)) diff --git a/rocolib/api/composables/graph/Graph.py b/rocolib/api/composables/graph/Graph.py index b8217b84a1009bf845d608c496c82d8650a5ef5c..5f11f921b3f034ad8fbcdf56bcc501510771b4c3 100644 --- a/rocolib/api/composables/graph/Graph.py +++ b/rocolib/api/composables/graph/Graph.py @@ -28,13 +28,12 @@ def inflate(face, thickness=.1, edges=False): return faces -def _triangulate(faces, **kwargs): +def _triangulate(faces, thickness=0): """Create triangulated faces in 3D space with thickness""" scale = .001 # roco units : mm ; STL units m tris = [] - thickness = kwargs.get("thickness", 0) nparr = lambda l: np.transpose([list(x) + [0, 1] for x in l]) for f in faces: @@ -80,6 +79,7 @@ class Graph(): self.faces = [] self.facelists = [] self.edges = [] + self._thickness = 0 self._meshes = [] self._fullmesh = None @@ -332,5 +332,60 @@ class Graph(): """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._meshes = [_triangulate(faces, self._thickness) for faces in self.facelists] self._fullmesh = mesh.Mesh(np.concatenate([m.data for m in self._meshes])) + + def getEdgeSet(self): + self.place() + edgeset = {} + + # Tile faces along x axis with buffer spacing between them + maxx = 0 + buffer = 10 + + for flist in self.facelists: + edges = set((e for face in flist for e in face.edges if e.pts2D is not None)) + + if not edges: + log.warning(f"No placed edges in flist, moving on.") + continue + + pts = [p for e in edges for p in e.pts2D] + minx = min([x[0] for x in pts]) + dx = maxx - minx + dy = 0 + + maxx = max([x[0] for x in pts]) + dx + buffer + + for e in edges: + if 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] + else: + angle = None + + edge = dict(p0 = e.pts2D[0] + [dx, dy], + p1 = e.pts2D[1] + [dx, dy], + faces = len(e.faces), + angle = angle, + length = e.length, + edgeType = e.edgeType, + interior = False, + ) + edgeset[e.name] = edge + + for face in flist: + for e in face.get2DDecorations(): + edge = dict(p0 = e[1] + [dx, dy], + p1 = e[2] + [dx, dy], + faces = None, + angle = None, + edgeType = e[3], + interior = True, + ) + edgeset[e[0]] = edge + + return edgeset diff --git a/rocolib/api/composables/graph/HyperEdge.py b/rocolib/api/composables/graph/HyperEdge.py index f7e60a8831a2f57b43506117eea99993249c5ce3..5122e64188ef902b1ef5827454009d3b953150c9 100644 --- a/rocolib/api/composables/graph/HyperEdge.py +++ b/rocolib/api/composables/graph/HyperEdge.py @@ -163,9 +163,6 @@ class HyperEdge: # 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) diff --git a/rocolib/utils/display.py b/rocolib/utils/display.py index 35b27834eed270d38048221640674f9fcc72fb2a..d696e8f5c5e04b2eee7e40975e2fe93fe930b3a4 100644 --- a/rocolib/utils/display.py +++ b/rocolib/utils/display.py @@ -42,7 +42,7 @@ def meshfig(stlmesh, scale=1000, color="#ccccff", axes=True): fig = go.Figure(data=[mesh3D], layout=layout) fig.update_layout(margin=dict(r=0, l=0, b=0, t=0), - scene_aspectmode='data', + scene_aspectmode='data', width=1024) fig.data[0].update(lighting=dict(ambient= 0.18, diffuse= 1, @@ -56,113 +56,111 @@ def meshfig(stlmesh, scale=1000, color="#ccccff", axes=True): return fig -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() +def linestyle(e, **kwargs): + from rocolib.api.composables.graph.DrawingEdge import EdgeType + + width = 3 + angle = e["angle"] + + if e["interior"]: # Interior decorations + width = 1 + if e["edgeType"] in ( EdgeType.BOUNDARY_CUT, EdgeType.INTERIOR_CUT ): + color = 'blue' + zorder = 30 + elif e["edgeType"] in ( EdgeType.FOLD, EdgeType.FLEX ): + color = 'red' + zorder = 20 + elif e["edgeType"] in ( EdgeType.FLAT ): + color = 'white' + zorder = 10 + else: # Other? + color = 'gray' + zorder = 100 + elif e["faces"] == 1: # CUT + color = 'blue' + zorder = 30 + elif e["faces"] == 2: # FOLD or FLEX or BEND + if angle: # FOLD + if angle > 0: # mountain fold + color = 'red' + zorder = 20 + else: # valley fold + color = 'green' + zorder = 20 + else: # FLEX or BEND or FLAT + color = 'white' + zorder = 10 + else: # 0 or 3+ faces? dunno. + color = 'grey' + zorder = 100 + + ret = dict(color = color, width = width, zorder = zorder) + ret.update(kwargs) + return ret + +def cutfig(edgeset, scale=1000, axes=True): + layout = go.Layout( + showlegend=False, + dragmode='pan', + yaxis=dict(scaleanchor="x", scaleratio=1), + margin=go.layout.Margin( + l=0, #left margin + r=0, #right margin + b=0, #bottom margin + t=0 #top margin + ) + ) + + fig = go.Figure(layout=layout) + traces = [] + + for n, e in edgeset.items(): + # Adding a trace with a fill, setting opacity to 0 + x0, y0 = e["p0"] + x1, y1 = e["p1"] + dx, dy = x1 - x0, y1 - y0 + length = (dx * dx + dy * dy) ** 0.5 + ddx = -dy/20 # hit box width = 10% length + ddy = dx/20 # hit box width = 10% length + + xs = [x0 - ddx, x0 + ddx, x1 + ddx, x1 - ddx] + ys = [y0 - ddy, y0 + ddy, y1 + ddy, y1 - ddy] + + hoverdata = "<br>".join([ + f"<b>Length</b>: {length}", + f"<b>Angle</b>: {e['angle']}", + f"<b>Faces</b>: {e['faces']}", + f"<b>Type</b>: {e['edgeType']}", + ]) + + style = linestyle(e) + zorder = style.pop("zorder") + + traces.append((zorder, + go.Scatter( + x = [x0, x1], + y = [y0, y1], + mode='lines', + name=n, + line=style, + ) + )) + + style["width"] = 0 + traces.append((zorder, + go.Scatter( + x = xs, + y = ys, + fill="toself", + mode='lines', + name=n, + text=hoverdata, + opacity=0, + line=style, + ) + )) + + for z, t in sorted(traces, key = lambda x : x[0]): + fig.add_trace(t) + + return fig