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