diff --git a/ShowSymmetries.glyphsReporter/Contents/Info.plist b/ShowSymmetries.glyphsReporter/Contents/Info.plist
new file mode 100644
index 0000000..c6d39a0
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/Info.plist
@@ -0,0 +1,90 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleDisplayName
+ ShowSymmetries
+ CFBundleExecutable
+ ShowSymmetries
+ CFBundleIdentifier
+ org.simon-cozens.ShowSymmetries
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ShowSymmetries
+ CFBundlePackageType
+ BNDL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 4
+ CFBundleShortVersionString
+ 1.1.1
+ UpdateFeedURL
+
+ productPageURL
+
+ LSHasLocalizedDisplayName
+
+ NSAppleScriptEnabled
+
+ NSHumanReadableCopyright
+ Copyright, by Simon Cozens, 2015
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ ShowSymmetries
+ PyMainFileNames
+
+ __boot__
+
+ PyOptions
+
+ alias
+
+ argv_emulation
+
+ no_chdir
+
+ optimize
+ 0
+ prefer_ppc
+
+ site_packages
+
+ use_pythonpath
+
+
+ PyResourcePackages
+
+ lib/python2.6
+ lib/python2.6/lib-dynload
+ lib/python2.6/site-packages.zip
+ lib/python26.zip
+
+ PyRuntimeLocations
+
+ @executable_path/../Frameworks/Python.framework/Versions/2.6/Python
+ /System/Library/Frameworks/Python.framework/Versions/2.6/Python
+
+ PythonInfoDict
+
+ PythonExecutable
+ /usr/bin/python2.6
+ PythonLongVersion
+ 2.6.7 (r267:88850, Oct 11 2012, 20:15:00)
+[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)]
+ PythonShortVersion
+ 2.6
+ py2app
+
+ template
+ bundle
+ version
+ 0.6.3
+
+
+
+
diff --git a/ShowSymmetries.glyphsReporter/Contents/MacOS/ShowSymmetries b/ShowSymmetries.glyphsReporter/Contents/MacOS/ShowSymmetries
new file mode 100755
index 0000000..b21f0e4
Binary files /dev/null and b/ShowSymmetries.glyphsReporter/Contents/MacOS/ShowSymmetries differ
diff --git a/ShowSymmetries.glyphsReporter/Contents/MacOS/python b/ShowSymmetries.glyphsReporter/Contents/MacOS/python
new file mode 120000
index 0000000..c84e063
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/MacOS/python
@@ -0,0 +1 @@
+/System/Library/Frameworks/Python.framework/Versions/2.6/bin/python
\ No newline at end of file
diff --git a/ShowSymmetries.glyphsReporter/Contents/PkgInfo b/ShowSymmetries.glyphsReporter/Contents/PkgInfo
new file mode 100644
index 0000000..19a9cf6
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/PkgInfo
@@ -0,0 +1 @@
+BNDL????
\ No newline at end of file
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/ShowSymmetries.py b/ShowSymmetries.glyphsReporter/Contents/Resources/ShowSymmetries.py
new file mode 100755
index 0000000..1b7edb4
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/Resources/ShowSymmetries.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python
+# encoding: utf-8
+import objc
+from Foundation import *
+from AppKit import *
+import sys, os, re
+from Quartz import CGContextGetCTM, CGAffineTransformInvert, CGContextConcatCTM, CGContextRestoreGState, CGContextSaveGState
+
+MainBundle = NSBundle.mainBundle()
+path = MainBundle.bundlePath() + "/Contents/Scripts"
+if not path in sys.path:
+ sys.path.append( path )
+
+import GlyphsApp
+import glyphmonkey
+from glyphmonkey import GSNodeSet
+
+GlyphsReporterProtocol = objc.protocolNamed( "GlyphsReporter" )
+
+class ShowSymmetries ( NSObject, GlyphsReporterProtocol ):
+
+ def init( self ):
+ bundle = NSBundle.bundleForClass_(ShowSymmetries)
+ self.rotational = bundle.imageForResource_("rotational")
+ self.reflectional = bundle.imageForResource_("reflectional")
+ self.reflecty = bundle.imageForResource_("reflecty")
+ return self
+
+ def interfaceVersion( self ):
+ return 1
+
+ def title( self ):
+ return "Symmetries"
+
+ def keyEquivalent( self ):
+ return None
+
+ def modifierMask( self ):
+ return 0
+
+ def drawForegroundForLayer_( self, Layer ):
+ pass
+
+ def drawSymmetries( self, Layer ):
+ currentZoom = self.getScale()
+ l = Layer.copy()
+ if l.pathCount() > 1:
+ l.removeOverlap()
+ ns = l.selectedNodeSet()
+ if len(ns) == 1: return
+ if len(ns) == 0:
+ sel = []
+ for p in Layer.paths:
+ for n in p.nodes:
+ sel.append(n)
+ ns = GSNodeSet(sel)
+
+ ox, oy = ns.center
+
+ height = self.controller.view().bounds().size.height
+ width = self.controller.view().bounds().size.width
+ context = NSGraphicsContext.currentContext().CGContext()
+ oldat = CGContextGetCTM(context)
+ CGContextSaveGState(context)
+ inverted = CGAffineTransformInvert(oldat)
+ CGContextConcatCTM(context, inverted)
+
+ if ns.equal(ns.copy().rotate(angle=180, ox=ox, oy=oy)):
+ self.rotational.drawInRect_(NSMakeRect(width-125,height-(15+25),25,25))
+
+ if ns.equal(ns.copy().reflect()):
+ self.reflectional.drawInRect_(NSMakeRect(width-95,height-(15+25),25,25))
+
+ if ns.equal(ns.copy().reflect(NSMakePoint(ox,oy), NSMakePoint(ox+100,oy))):
+ self.reflecty.drawInRect_(NSMakeRect(width-65,height-(15+25),25,25))
+
+ CGContextConcatCTM(context, oldat)
+ CGContextRestoreGState(context)
+
+ def drawBackgroundForLayer_( self, Layer ):
+ try:
+ self.drawSymmetries( Layer )
+ except Exception as e:
+ self.logToConsole( "drawBackgroundForLayer_: %s" % str(e) )
+
+ def drawBackgroundForInactiveLayer_( self, Layer ):
+ pass
+
+ def drawTextAtPoint( self, text, textPosition, fontSize=10.0, fontColor=NSColor.colorWithCalibratedRed_green_blue_alpha_( 1, 0, .5, 1 ) ):
+ """
+ Use self.drawTextAtPoint( "blabla", myNSPoint ) to display left-aligned text at myNSPoint.
+ """
+ try:
+ glyphEditView = self.controller.graphicView()
+ currentZoom = self.getScale()
+ fontAttributes = {
+ NSFontAttributeName: NSFont.labelFontOfSize_( fontSize/currentZoom ),
+ NSForegroundColorAttributeName: fontColor }
+ displayText = NSAttributedString.alloc().initWithString_attributes_( text, fontAttributes )
+ textAlignment = 2 # top left: 6, top center: 7, top right: 8, center left: 3, center center: 4, center right: 5, bottom left: 0, bottom center: 1, bottom right: 2
+ glyphEditView.drawText_atPoint_alignment_( displayText, textPosition, textAlignment )
+ except Exception as e:
+ self.logToConsole( "drawTextAtPoint: %s" % str(e) )
+
+ def needsExtraMainOutlineDrawingForInactiveLayer_( self, Layer ):
+ return True
+
+ def getHandleSize( self ):
+ """
+ Returns the current handle size as set in user preferences.
+ Use: self.getHandleSize() / self.getScale()
+ to determine the right size for drawing on the canvas.
+ """
+ try:
+ Selected = NSUserDefaults.standardUserDefaults().integerForKey_( "GSHandleSize" )
+ if Selected == 0:
+ return 5.0
+ elif Selected == 2:
+ return 10.0
+ else:
+ return 7.0 # Regular
+ except Exception as e:
+ self.logToConsole( "getHandleSize: HandleSize defaulting to 7.0. %s" % str(e) )
+ return 7.0
+
+ def getScale( self ):
+ """
+ self.getScale() returns the current scale factor of the Edit View UI.
+ Divide any scalable size by this value in order to keep the same apparent pixel size.
+ """
+ try:
+ return self.controller.graphicView().scale()
+ except:
+ self.logToConsole( "Scale defaulting to 1.0" )
+ return 1.0
+
+ def setController_( self, Controller ):
+ """
+ Use self.controller as object for the current view controller.
+ """
+ try:
+ self.controller = Controller
+ except Exception as e:
+ self.logToConsole( "Could not set controller" )
+
+ def logToConsole( self, message ):
+ """
+ The variable 'message' will be passed to Console.app.
+ Use self.logToConsole( "bla bla" ) for debugging.
+ """
+ myLog = "Show %s plugin:\n%s" % ( self.title(), message )
+ NSLog( myLog )
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/__boot__.py b/ShowSymmetries.glyphsReporter/Contents/Resources/__boot__.py
new file mode 100644
index 0000000..86a4db8
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/Resources/__boot__.py
@@ -0,0 +1,47 @@
+# def _site_packages():
+# import site, sys, os
+# paths = []
+# prefixes = [sys.prefix]
+# if sys.exec_prefix != sys.prefix:
+# prefixes.append(sys.exec_prefix)
+# for prefix in prefixes:
+# if prefix == sys.prefix:
+# paths.append(os.path.join("/Library/Python", sys.version[:3], "site-packages"))
+# paths.append(os.path.join(sys.prefix, "Extras", "lib", "python"))
+# else:
+# paths.append(os.path.join(prefix, 'lib', 'python' + sys.version[:3], 'site-packages'))
+# if os.path.join('.framework', '') in os.path.join(sys.prefix, ''):
+# home = os.environ.get('HOME')
+# if home:
+# paths.append(os.path.join(home, 'Library', 'Python', sys.version[:3], 'site-packages'))
+#
+# # Workaround for a misfeature in setuptools: easy_install.pth places
+# # site-packages way too early on sys.path and that breaks py2app bundles.
+# # NOTE: this hacks into an undocumented feature of setuptools and
+# # might stop to work without warning.
+# sys.__egginsert = len(sys.path)
+#
+# for path in paths:
+# site.addsitedir(path)
+#
+# _site_packages()
+#
+# def _path_inject():
+# import sys
+# sys.path[:0] = sys.path[0]
+#
+# _path_inject()
+
+def _run(*scripts):
+ global __file__
+ import os, sys# , site
+ sys.frozen = 'macosx_plugin'
+ base = os.environ['RESOURCEPATH']
+ # site.addsitedir(base)
+ # site.addsitedir(os.path.join(base, 'Python', 'site-packages'))
+ for script in scripts:
+ path = os.path.join(base, script)
+ __file__ = path
+ execfile(path, globals(), globals())
+
+_run('ShowSymmetries.py')
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/__error__.sh b/ShowSymmetries.glyphsReporter/Contents/Resources/__error__.sh
new file mode 100755
index 0000000..b3fc73d
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/Resources/__error__.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+#
+# This is the default bundletemplate error script
+# Note that this DOES NOT present a GUI dialog, because
+# it has no output on stdout, and has a return value of 0.
+#
+if ( test -n "$2" ) ; then
+ echo "[$1] Unexpected Exception:" 1>&2
+ echo "$2: $3" 1>&2
+else
+ echo "[$1] Could not find a suitable Python runtime" 1>&2
+fi
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/glyphmonkey.py b/ShowSymmetries.glyphsReporter/Contents/Resources/glyphmonkey.py
new file mode 100644
index 0000000..8f90058
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/Resources/glyphmonkey.py
@@ -0,0 +1,342 @@
+#MenuTitle:
+# -*- coding: utf-8 -*-
+__doc__=""
+import GlyphsApp
+from GlyphsApp import Proxy
+from math import atan2, sqrt, cos, sin, radians
+from Foundation import NSMakePoint, NSValue, NSMakeRect
+
+# Make GSNodes hashable
+
+class GSLineSegment(object):
+ def __init__(self, tuple = None, owner = None, idx = 0):
+ self._seg = tuple
+ if not self._seg: self._seg = owner._segments[idx]
+ self._owner = owner
+ self._owneridx = idx
+
+ def __repr__(self):
+ """Return list-lookalike of representation string of objects"""
+ return "" % (self.start.x,self.start.y,self.end.x,self.end.y)
+
+ def _seg(self): return self._seg
+ @property
+ def start(self): return self._seg[0].position
+ @property
+ def end(self): return self._seg[-1].position
+
+ # For backward compatibility
+ def __getitem__(self, Key):
+ if Key < 0:
+ Key = self.__len__() + Key
+ # There is a horribly subtle distinction between an NSValue and
+ # an NSPoint. SpeedPunk expects to see an NSValue here and dies
+ # if it doesn't have one.
+ return NSValue.valueWithPoint_(self._seg[Key].position)
+
+ @property
+ def area(self):
+ xa, ya = self.start.x, self.start.y/20
+ xb, yb = xa, ya
+ xc, yc = self.end.x, self.end.y/20
+ xd, yd = xc, yc
+ return (xb-xa)*(10*ya + 6*yb + 3*yc + yd) + (xc-xb)*( 4*ya + 6*yb + 6*yc + 4*yd) +(xd-xc)*( ya + 3*yb + 6*yc + 10*yd)
+
+ @property
+ def length(self):
+ l1 = self.start.x - self.end.x
+ l2 = self.start.y - self.end.y
+ return sqrt(l1 * l1 + l2 * l2)
+
+ @property
+ def angle(self):
+ e = self.end
+ s = self.start
+ return atan2(e.y - s.y, e.x - s.x)
+
+ @property
+ def selected(self):
+ return self.start.selected and self.end.selected
+
+ def __len__(self):
+ return 2
+
+class GSCurveSegment(GSLineSegment):
+ def __repr__(self):
+ return "" % (
+ self.start.x, self.start.y,
+ self.handle1.x, self.handle1.y,
+ self.handle2.x, self.handle2.y,
+ self.end.x,self.end.y
+ )
+
+ @property
+ def handle1(self): return self._seg[1].position
+ @property
+ def handle2(self): return self._seg[2].position
+
+ def area(self):
+ xa, ya = self.start.x, self.start.y/20
+ xb, yb = self.handle1.x, self.handle1.y/20
+ xc, yc = self.handle2.x, self.handle2.y/20
+ xd, yd = self.end.x, self.end.y/20
+ return (xb-xa)*(10*ya + 6*yb + 3*yc + yd) + (xc-xb)*( 4*ya + 6*yb + 6*yc + 4*yd) +(xd-xc)*( ya + 3*yb + 6*yc + 10*yd)
+
+ def angle(self):
+ e = self.end
+ s = self.start
+ return atan2(e.y - s.y, e.x - s.x)
+
+ def interpolate_at_fraction(self, t):
+ if t < 0 or t > 1:
+ raise Exception("interpolate_at_fraction should be called with a number between 0 and 1")
+ t1 = 1.0 - t;
+ t1_3 = t1*t1*t1
+ t1_3a = (3*t)*(t1*t1)
+ t1_3b = (3*(t*t))*t1;
+ t1_3c = (t * t * t )
+ x = (self.start.x * t1_3) + (t1_3a * self.handle1.x) + (t1_3b * self.handle2.x) + (t1_3c * self.end.x)
+ y = (self.start.y * t1_3) + (t1_3a * self.handle1.y) + (t1_3b * self.handle2.y) + (t1_3c * self.end.y)
+ return (x,y)
+
+ @property
+ def length(self):
+ steps = 50
+ t = 0.0
+ length = 0
+ previous = ()
+ while t < 1.0:
+ this = self.interpolate_at_fraction(t)
+ if t > 0:
+ dx = previous[0] - this[0]
+ dy = previous[1] - this[1]
+ length = length + sqrt(dx*dx+dy*dy)
+ t = t + 1.0/steps
+ previous = this
+ return length
+
+ def __len__(self):
+ return 4
+
+class PathSegmentsProxy (Proxy):
+ # Actually we're not going to use .segments at all, because we
+ # want to be able to access things like GSNode.selected
+ def toSegments(p):
+ segList = []
+ nodeList = p._owner.nodes
+ thisSeg = (nodeList[-1],)
+ for i in range(0,len(nodeList)):
+ thisSeg = thisSeg + (nodeList[i],)
+ if nodeList[i].type != GlyphsApp.GSOFFCURVE:
+ segList.append(thisSeg)
+ thisSeg = (nodeList[i],)
+ return segList
+ def __getitem__(self, Key):
+ if Key < 0:
+ Key = self.__len__() + Key
+ segs = self.toSegments()
+ if len(segs[Key]) == 2:
+ return GSLineSegment( owner = self._owner, idx = Key, tuple = segs[Key])
+ else:
+ return GSCurveSegment( owner = self._owner, idx = Key, tuple = segs[Key])
+ def __setitem__(self, Key, Layer):
+ if Key < 0:
+ Key = self.__len__() + Key
+ # XXX
+ def __len__(self):
+ return len(self.toSegments())
+ def values(self):
+ return map(self.__getitem__, range(0,self.__len__()))
+
+# Unfortunately working with segments doesn't always *work*. So we
+# map a segment list to a node list
+GSNode = GlyphsApp.GSNode
+
+def toNodeList(segments):
+ nodelist = []
+ for i in range(0,len(segments)):
+ s = segments[i]
+ t = GlyphsApp.GSCURVE
+ c = GlyphsApp.GSSMOOTH
+ if type(s) is GSLineSegment:
+ t = GlyphsApp.GSLINE
+ else:
+ s1 = s.handle1
+ nodelist.append(GSNode((s1.x,s1.y), GlyphsApp.GSOFFCURVE))
+ s2 = s.handle2
+ nodelist.append(GSNode((s2.x,s2.y), GlyphsApp.GSOFFCURVE))
+
+ ns = i+1
+ if ns >= len(segments): ns = 0
+ if type(segments[ns]) is GSLineSegment:
+ c = GlyphsApp.GSSHARP
+ e = s.end
+ node = GSNode((e.x, e.y), t)
+ node.connection = c
+ nodelist.append(node)
+ return nodelist
+
+GlyphsApp.GSPath.segments = property( lambda self: PathSegmentsProxy(self),
+ lambda self, value:
+ self.setNodes_(toNodeList(value))
+)
+
+def nodeRotate(self, ox, oy, angle):
+ angle = radians(angle)
+ newX = ox + (self.position.x-ox)*cos(angle) - (self.position.y-oy)*sin(angle)
+ newY = oy + (self.position.x-ox)*sin(angle) + (self.position.y-oy)*cos(angle)
+ self.position = (round(newX,2), round(newY,2))
+
+def nodeReflect(self, p0, p1):
+ dx = p1.x - p0.x
+ dy = p1.y - p0.y
+ a = (dx * dx - dy * dy) / (dx * dx + dy * dy)
+ b = 2 * dx * dy / (dx * dx + dy * dy)
+ x = a * (self.position.x - p0.x) + b * (self.position.y - p0.y) + p0.x
+ y = b * (self.position.x - p0.x) - a * (self.position.y - p0.y) + p0.y
+ self.position =(round(x,2), round(y,2))
+
+GlyphsApp.GSNode.rotate = nodeRotate
+GlyphsApp.GSNode.reflect = nodeReflect
+
+### additional GSPath methods
+
+def layerCenter(self):
+ bounds = self.parent.bounds
+ ox = bounds.origin.x + bounds.size.width / 2
+ oy = bounds.origin.y + bounds.size.height / 2
+ return NSMakePoint(ox, oy)
+
+def pathCenter(self):
+ bounds = self.bounds
+ ox = bounds.origin.x + bounds.size.width / 2
+ oy = bounds.origin.y + bounds.size.height / 2
+ return NSMakePoint(ox, oy)
+
+def pathRotate(self, angle=-1, ox=-1, oy=-1):
+ if angle == -1: angle = 180
+ if ox == -1 and oy == -1:
+ if self.parent: # Almost always
+ ox, oy = self.layerCenter().x, self.layerCenter().y
+ else:
+ ox, oy = self.center().x, self.center().y
+
+ for n in self.nodes:
+ n.rotate(ox, oy, angle)
+ return self
+
+def pathReflect(self, p0 = -1, p1 = -1):
+ if p0 == -1 and p1 == -1:
+ if self.parent: # Almost always
+ p0 = self.layerCenter()
+ p1 = self.layerCenter()
+ else:
+ p0 = self.center()
+ p1 = self.center()
+ p1.y = p1.y + 100
+
+ for n in self.nodes:
+ n.reflect(p0, p1)
+ return self
+
+def pathDiff(p1, p2):
+ nodes1 = set((n.position.x,n.position.y) for n in p1.nodes)
+ nodes2 = set((n.position.x,n.position.y) for n in p2.nodes)
+ return nodes1 - nodes2
+
+def pathEqual(p1, p2):
+ pd = pathDiff(p1, p2)
+ return len(pd) == 0
+
+def pathToNodeSet(self):
+ return GSNodeSet(self.nodes)
+
+GlyphsApp.GSPath.layerCenter = layerCenter
+GlyphsApp.GSPath.center = pathCenter
+GlyphsApp.GSPath.rotate = pathRotate
+GlyphsApp.GSPath.reflect = pathReflect
+GlyphsApp.GSPath.equal = pathEqual
+GlyphsApp.GSPath.diff = pathDiff
+GlyphsApp.GSPath.toNodeSet = pathToNodeSet
+
+class GSNodeSet(object):
+ def toKey(self,n):
+ return "%s %s %s" % (n.position.x, n.position.y, n.type)
+
+ def __init__(self, nodes):
+ self._dict = {}
+ for n in nodes:
+ self._dict[self.toKey(n)] = n
+
+ def __repr__(self):
+ return "" % (len(self))
+
+ def __len__(self):
+ return len(self._dict)
+
+ @property
+ def nodes(self):
+ return self._dict.values()
+
+ @property
+ def bounds(self):
+ minx, maxx, miny, maxy = None, None, None, None
+ if len(self) < 1: return None
+ for p in self.nodes:
+ pos = p.position
+ if minx == None or pos.x < minx: minx = pos.x
+ if maxx == None or pos.x > maxx: maxx = pos.x
+ if miny == None or pos.y < minx: miny = pos.y
+ if maxy == None or pos.y > maxx: maxy = pos.y
+
+ return NSMakeRect(minx, miny, maxx-minx, maxy-miny)
+
+ @property
+ def center(self):
+ if len(self) < 1: return None
+ b = self.bounds
+ return NSMakePoint(b.origin.x + b.size.width / 2, b.origin.y + b.size.height / 2, )
+
+ def copy(self):
+ return GSNodeSet(n.copy() for n in self.nodes)
+
+ def diff(ns1, ns2):
+ nodes1 = set((n.position.x,n.position.y) for n in ns1.nodes)
+ nodes2 = set((n.position.x,n.position.y) for n in ns2.nodes)
+ return nodes1 - nodes2
+
+ def equal(p1, p2):
+ pd = p1.diff(p2)
+ return len(pd) == 0
+
+ def rotate(self, angle=-1, ox=-1, oy=-1):
+ if angle == -1: angle = 180
+ if ox == -1 and oy == -1:
+ ox, oy = self.center.x, self.center.y
+
+ for n in self.nodes:
+ n.rotate(ox, oy, angle)
+ return self
+
+ def reflect(self, p0 = -1, p1 = -1):
+ if p0 == -1 and p1 == -1:
+ p0 = self.center
+ p1 = self.center
+ p1.y = p1.y + 100
+
+ for n in self.nodes:
+ n.reflect(p0, p1)
+ return self
+
+def selectedNodeSet(layer):
+ sel = []
+ for n in layer.selection:
+ if isinstance(n, GSNode):
+ sel.append(n)
+ return GSNodeSet(sel)
+
+GlyphsApp.GSLayer.selectedNodeSet = selectedNodeSet
+
+# Does p have rotational symmetry?
+# ox, oy = p.layerCenter()
+# p.equal(p.copy().rotate(angle=180, ox=ox, oy=oy))
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/glyphmonkey.pyc b/ShowSymmetries.glyphsReporter/Contents/Resources/glyphmonkey.pyc
new file mode 100644
index 0000000..c4423c4
Binary files /dev/null and b/ShowSymmetries.glyphsReporter/Contents/Resources/glyphmonkey.pyc differ
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/reflectional.png b/ShowSymmetries.glyphsReporter/Contents/Resources/reflectional.png
new file mode 100644
index 0000000..0ac9e5e
Binary files /dev/null and b/ShowSymmetries.glyphsReporter/Contents/Resources/reflectional.png differ
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/reflecty.png b/ShowSymmetries.glyphsReporter/Contents/Resources/reflecty.png
new file mode 100644
index 0000000..1723181
Binary files /dev/null and b/ShowSymmetries.glyphsReporter/Contents/Resources/reflecty.png differ
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/rotational.png b/ShowSymmetries.glyphsReporter/Contents/Resources/rotational.png
new file mode 100644
index 0000000..5602866
Binary files /dev/null and b/ShowSymmetries.glyphsReporter/Contents/Resources/rotational.png differ
diff --git a/ShowSymmetries.glyphsReporter/Contents/Resources/site.py b/ShowSymmetries.glyphsReporter/Contents/Resources/site.py
new file mode 100644
index 0000000..889eb41
--- /dev/null
+++ b/ShowSymmetries.glyphsReporter/Contents/Resources/site.py
@@ -0,0 +1,130 @@
+"""
+Append module search paths for third-party packages to sys.path.
+
+This is stripped down and customized for use in py2app applications
+"""
+
+import sys
+# os is actually in the zip, so we need to do this here.
+# we can't call it python24.zip because zlib is not a built-in module (!)
+_libdir = '/lib/python' + sys.version[:3]
+_parent = '/'.join(__file__.split('/')[:-1])
+if not _parent.endswith(_libdir):
+ _parent += _libdir
+sys.path.append(_parent + '/site-packages.zip')
+
+# Stuffit decompresses recursively by default, that can mess up py2app bundles,
+# add the uncompressed site-packages to the path to compensate for that.
+sys.path.append(_parent + '/site-packages')
+
+import os
+try:
+ basestring
+except NameError:
+ basestring = str
+
+def makepath(*paths):
+ dir = os.path.abspath(os.path.join(*paths))
+ return dir, os.path.normcase(dir)
+
+for m in sys.modules.values():
+ f = getattr(m, '__file__', None)
+ if isinstance(f, basestring) and os.path.exists(f):
+ m.__file__ = os.path.abspath(m.__file__)
+del m
+
+# This ensures that the initial path provided by the interpreter contains
+# only absolute pathnames, even if we're running from the build directory.
+L = []
+_dirs_in_sys_path = {}
+dir = dircase = None # sys.path may be empty at this point
+for dir in sys.path:
+ # Filter out duplicate paths (on case-insensitive file systems also
+ # if they only differ in case); turn relative paths into absolute
+ # paths.
+ dir, dircase = makepath(dir)
+ if not dircase in _dirs_in_sys_path:
+ L.append(dir)
+ _dirs_in_sys_path[dircase] = 1
+sys.path[:] = L
+del dir, dircase, L
+_dirs_in_sys_path = None
+
+def _init_pathinfo():
+ global _dirs_in_sys_path
+ _dirs_in_sys_path = d = {}
+ for dir in sys.path:
+ if dir and not os.path.isdir(dir):
+ continue
+ dir, dircase = makepath(dir)
+ d[dircase] = 1
+
+def addsitedir(sitedir):
+ global _dirs_in_sys_path
+ if _dirs_in_sys_path is None:
+ _init_pathinfo()
+ reset = 1
+ else:
+ reset = 0
+ sitedir, sitedircase = makepath(sitedir)
+ if not sitedircase in _dirs_in_sys_path:
+ sys.path.append(sitedir) # Add path component
+ try:
+ names = os.listdir(sitedir)
+ except os.error:
+ return
+ names.sort()
+ for name in names:
+ if name[-4:] == os.extsep + "pth":
+ addpackage(sitedir, name)
+ if reset:
+ _dirs_in_sys_path = None
+
+def addpackage(sitedir, name):
+ global _dirs_in_sys_path
+ if _dirs_in_sys_path is None:
+ _init_pathinfo()
+ reset = 1
+ else:
+ reset = 0
+ fullname = os.path.join(sitedir, name)
+ try:
+ f = open(fullname)
+ except IOError:
+ return
+ while 1:
+ dir = f.readline()
+ if not dir:
+ break
+ if dir[0] == '#':
+ continue
+ if dir.startswith("import"):
+ exec(dir)
+ continue
+ if dir[-1] == '\n':
+ dir = dir[:-1]
+ dir, dircase = makepath(sitedir, dir)
+ if not dircase in _dirs_in_sys_path and os.path.exists(dir):
+ sys.path.append(dir)
+ _dirs_in_sys_path[dircase] = 1
+ if reset:
+ _dirs_in_sys_path = None
+
+
+#sys.setdefaultencoding('utf-8')
+
+#
+# Run custom site specific code, if available.
+#
+try:
+ import sitecustomize
+except ImportError:
+ pass
+
+#
+# Remove sys.setdefaultencoding() so that users cannot change the
+# encoding after initialization. The test for presence is needed when
+# this module is run as a script, because this code is executed twice.
+#
+if hasattr(sys, "setdefaultencoding"):
+ del sys.setdefaultencoding