Initial import
authorAntonio Ospite <ao2@ao2.it>
Fri, 8 May 2015 14:45:15 +0000 (16:45 +0200)
committerAntonio Ospite <ao2@ao2.it>
Fri, 8 May 2015 14:49:43 +0000 (16:49 +0200)
.gitignore [new file with mode: 0644]
Diagram.py [new file with mode: 0755]
Makefile [new file with mode: 0644]
RadialSymmetry.py [new file with mode: 0755]
RadialSymmetryDiagram.py [new file with mode: 0755]
TODO [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/Diagram.py b/Diagram.py
new file mode 100755 (executable)
index 0000000..0bc3925
--- /dev/null
@@ -0,0 +1,277 @@
+#!/usr/bin/env python
+#
+# A Diagram abstraction based on Cairo
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+from math import *
+
+
+class Diagram(object):
+
+    def __init__(self, width, height, background=[1, 1, 1]):
+        self.width = width
+        self.height = height
+        self.background = background
+
+        # TODO: use a RecordingSurface
+        self.surface = cairo.SVGSurface(None, width, height)
+        self.cr = cr = cairo.Context(self.surface)
+
+        # convert to left-bottom-origin cartesian coordinates
+        cr.translate(0, self.height)
+        cr.scale(1, -1)
+
+        cr.select_font_face("Georgia", cairo.FONT_SLANT_NORMAL,
+                            cairo.FONT_WEIGHT_NORMAL)
+        cr.set_font_size(20)
+
+        # Adjust the font matrix to left-bottom origin
+        M = cr.get_font_matrix()
+        M.scale(1, -1)
+        cr.set_font_matrix(M)
+
+    def clear(self):
+        cr = self.cr
+
+        r, g, b, a = self.color_to_rgba(self.background)
+        cr.set_source_rgba(r, g, b, a)
+        cr.paint()
+
+    def save_svg(self, filename):
+        surface = cairo.SVGSurface(filename + '.svg', self.width, self.height)
+        cr = cairo.Context(surface)
+        cr.set_source_surface(self.surface, 0, 0)
+        cr.paint()
+
+    def save_png(self, filename):
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height)
+        cr = cairo.Context(surface)
+        cr.set_source_surface(self.surface, 0, 0)
+        cr.paint()
+        surface.write_to_png(filename + '.png')
+
+    def show(self):
+        import Image
+        import StringIO
+        f = StringIO.StringIO()
+        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height)
+        cr = cairo.Context(surface)
+        cr.set_source_surface(self.surface, 0, 0)
+        cr.paint()
+        surface.write_to_png(f)
+        f.seek(0)
+        im = Image.open(f)
+        im.show()
+
+    def get_regular_polygon(self, x, y, sides, r, theta0=0.0):
+        theta = 2 * pi / sides
+
+        verts = []
+        for i in range(0, sides):
+            px = x + r * sin(theta0 + i * theta)
+            py = y + r * cos(theta0 + i * theta)
+            verts.append((px, py))
+
+        return verts
+
+    def color_to_rgba(self, color):
+        if len(color) == 3:
+            return color[0], color[1], color[2], 1.0
+        elif len(color) == 4:
+            return color[0], color[1], color[2], color[3]
+        else:
+            return None
+
+    def draw_polygon(self, verts, stroke_color=[0, 0, 0], fill_color=None):
+        cr = self.cr
+
+        if fill_color:
+            v = verts[0]
+            cr.move_to(v[0], v[1])
+            for v in verts[1:]:
+                cr.line_to(v[0], v[1])
+                cr.close_path()
+
+            r, g, b, a = self.color_to_rgba(fill_color)
+            cr.set_source_rgba(r, g, b, a)
+            cr.fill()
+
+        n = len(verts)
+        for i in range(0, n):
+            v1 = verts[i]
+            v2 = verts[(i + 1) % n]
+            cr.move_to(v1[0], v1[1])
+            cr.line_to(v2[0], v2[1])
+
+        r, g, b, a = self.color_to_rgba(stroke_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.stroke()
+
+    def draw_star(self, cx, cy, verts, stroke_color=[0, 0, 0]):
+        cr = self.cr
+
+        v = verts[0]
+        cr.move_to(cx, cy)
+        for v in verts:
+            cr.line_to(v[0], v[1])
+            cr.move_to(cx, cy)
+
+        r, g, b, a = self.color_to_rgba(stroke_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.stroke()
+
+    def draw_dot(self, cx, cy, size=10.0, fill_color=[0, 0, 0, 0.5]):
+        cr = self.cr
+
+        cr.save()
+        cr.set_source_rgba(fill_color[0], fill_color[1], fill_color[2],
+                           fill_color[3])
+        cr.arc(cx, cy, size, 0, 2 * pi)
+        cr.fill()
+        cr.restore()
+
+    def normalized_angle_01(self, theta):
+        return fmod(theta, 2 * pi) / (2 * pi)
+
+    def draw_line(self, x1, y1, x2, y2, stroke_color=[0, 0, 0, 1]):
+        cr = self.cr
+        r, g, b, a = self.color_to_rgba(stroke_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.move_to(x1, y1)
+        cr.line_to(x2, y2)
+        cr.stroke()
+
+    def draw_rect_from_center(self, cx, cy, width, height, theta=0,
+                              fill=True, fill_color=[1, 1, 1],
+                              stroke=False, stroke_color=[0, 0, 0]):
+        cr = self.cr
+
+        mx = width / 2.0
+        my = height / 2.0
+
+        rx = cx - (mx * cos(theta) - my * sin(theta))
+        ry = cy - (mx * sin(theta) + my * cos(theta))
+
+        cr.save()
+        cr.translate(rx, ry)
+        cr.rotate(theta)
+
+        if fill:
+            cr.rectangle(0, 0, width, height)
+            cr.set_source_rgba(fill_color[0], fill_color[1], fill_color[2], 0.8)
+            cr.fill()
+
+        if stroke:
+            cr.rectangle(0, 0, width, height)
+            cr.set_source_rgba(stroke_color[0], stroke_color[1], stroke_color[2], 0.5)
+            cr.stroke()
+            self.draw_dot(0, 0, 3.0, list(stroke_color) + [0.5])
+
+        cr.restore()
+
+    def draw_rect(self, x, y, width, height, fill=True, fill_color=[1, 1, 1],
+                  stroke=False, stroke_color=[0, 0, 0]):
+        cr = self.cr
+
+        cr.save()
+        cr.translate(x, y)
+
+        if fill:
+            cr.rectangle(0, 0, width, height)
+            cr.set_source_rgba(fill_color[0], fill_color[1], fill_color[2], 0.8)
+            cr.fill()
+
+        if stroke:
+            cr.rectangle(0, 0, width, height)
+            cr.set_source_rgba(stroke_color[0], stroke_color[1], stroke_color[2], 0.5)
+            cr.stroke()
+            self.draw_dot(0, 0, 3.0, list(stroke_color) + [0.5])
+
+        cr.restore()
+
+    def draw_centered_text(self, cx, cy, text, theta=0, color=[0, 0, 0], bounding_box=False):
+        cr = self.cr
+
+        x_bearing, y_bearing, width, height, x_advance = cr.text_extents(text)[:5]
+        ascent, descent = cr.font_extents()[:2]
+
+        # The offset of the lower-left corner of the text.
+        # NOTE: y is kept on the baseline
+        tx = width / 2.0 + x_bearing
+        ty = 0
+
+        # Angles are intended clockwise by the caller, but the trigonometric
+        # functions below consider angles counter-clockwise
+        theta = -theta
+
+        # The coordinate of the lower-left corner of the rotated rectangle
+        rx = cx - tx * cos(theta) + ty * sin(theta)
+        ry = cy - tx * sin(theta) - ty * cos(theta)
+
+        cr.save()
+        cr.translate(rx, ry)
+        cr.rotate(theta)
+        self.draw_rect(0, -descent, width, ascent, fill_color=[1, 1, 1, 0.1], stroke=bounding_box)
+        cr.set_source_rgba(color[0], color[1], color[2], 0.8)
+        cr.move_to(0, 0)
+        cr.show_text(text)
+        cr.fill()
+        cr.restore()
+
+        return x_advance
+
+
+if __name__ == "__main__":
+    diagram = Diagram(400, 400)
+
+    diagram.clear()
+
+    x = 40
+    y = 200
+
+    x_offset = x
+
+    theta = 0
+
+    advance = diagram.draw_centered_text(x_offset, y, "Ciao", theta, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "____", theta + pi / 4, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "jxpqdlf", theta + pi / 2, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "pppp", theta + 3 * pi / 4, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "dddd", theta + pi, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "Jjjj", theta + 5 * pi / 4, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "1369", theta + 3 * pi / 2, bounding_box=True)
+    x_offset += advance
+
+    advance = diagram.draw_centered_text(x_offset, y, "qqqq", theta + 7 * pi / 4, bounding_box=True)
+    x_offset += advance
+
+    diagram.draw_line(0, y, 400, y, [0, 0, 1, 0.2])
+
+    diagram.show()
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..9fab454
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+all:
+       ./RadialSymmetry.py
+
+test:
+       ./RadialSymmetryDiagram.py
+
+pep8:
+       pep8 --ignore=E501 *.py
+
+clean:
+       rm -f *.pyc radial_symmetry_test.svg radial_symmetry_test.png
diff --git a/RadialSymmetry.py b/RadialSymmetry.py
new file mode 100755 (executable)
index 0000000..aba522e
--- /dev/null
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+#
+# An app to interactively change RadialSymmetryDiagram properies
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from gi.repository import Gtk, Gio
+import sys
+import math
+
+import RadialSymmetryDiagram
+
+APPLICATION_ID = "it.ao2.RadialSymmetryApp"
+WINDOW_TITLE = "Radial Symmetry"
+CANVAS_WIDTH = 600
+CANVAS_HEIGHT = 600
+
+
+class RadialSymmetryWindow(Gtk.ApplicationWindow):
+
+    def __init__(self):
+        Gtk.Window.__init__(self, title=WINDOW_TITLE)
+        self.set_border_width(10)
+        self.set_property('resizable', False)
+
+        self.model = model = RadialSymmetryDiagram.RadialSymmetryModel()
+        self.diagram = RadialSymmetryDiagram.RadialSymmetryDiagram(CANVAS_WIDTH,
+                                                                   CANVAS_HEIGHT)
+        model.base_polygon_sides = 6
+        ad = Gtk.Adjustment(model.base_polygon_sides, 1, 32, 1, 0, 0)
+        self.spin_base_polygon_sides = Gtk.SpinButton(adjustment=ad, climb_rate=1, digits=0)
+        self.spin_base_polygon_sides.connect("value-changed", self.get_base_polygon_sides)
+
+        model.element_sides = 4
+        ad = Gtk.Adjustment(model.element_sides, 2, 16, 1, 0, 0)
+        self.spin_elem_sides = Gtk.SpinButton(adjustment=ad, climb_rate=1, digits=0)
+        self.spin_elem_sides.connect("value-changed", self.get_elem_sides)
+
+        max_element_radius = min(CANVAS_WIDTH, CANVAS_HEIGHT)
+        model.element_radius = max_element_radius / 4.0
+        ad = Gtk.Adjustment(model.element_radius, 0, max_element_radius, 1, 0, 0)
+        self.spin_element_radius = Gtk.SpinButton(adjustment=ad, climb_rate=1, digits=0)
+        self.spin_element_radius.connect("value-changed", self.get_element_radius)
+
+        model.radial_orientate = True
+        self.checkbox_radial_orientate = Gtk.CheckButton(label="Radial orientation")
+        self.checkbox_radial_orientate.set_active(model.radial_orientate)
+        self.checkbox_radial_orientate.connect("toggled", self.get_radial_orientate)
+
+        model.show_base_polygon = False
+        self.checkbox_base_polygon = Gtk.CheckButton(label="Draw base polygon")
+        self.checkbox_base_polygon.set_active(model.show_base_polygon)
+        self.checkbox_base_polygon.connect("toggled", self.get_base_polygon)
+
+        max_base_polygon_radius = min(CANVAS_WIDTH, CANVAS_HEIGHT)
+        model.base_polygon_radius = max_base_polygon_radius / 4.0
+        ad = Gtk.Adjustment(model.base_polygon_radius, 0, max_base_polygon_radius, 1, 0, 0)
+        self.spin_base_polygon_radius = Gtk.SpinButton(adjustment=ad, climb_rate=1, digits=0)
+        self.spin_base_polygon_radius.connect("value-changed", self.get_base_polygon_radius)
+
+        model.show_stars = False
+        self.checkbox_stars = Gtk.CheckButton(label="Draw stars")
+        self.checkbox_stars.set_active(model.show_stars)
+        self.checkbox_stars.connect("toggled", self.get_draw_stars)
+
+        model.show_elements = True
+        self.checkbox_elements = Gtk.CheckButton(label="Draw elements")
+        self.checkbox_elements.set_active(model.show_elements)
+        self.checkbox_elements.connect("toggled", self.get_draw_elements)
+
+        model.show_labels = False
+        self.checkbox_labels = Gtk.CheckButton(label="Draw labels")
+        self.checkbox_labels.set_active(model.show_labels)
+        self.checkbox_labels.connect("toggled", self.get_draw_labels)
+
+        model.element_angle_offset = 0
+        ad2 = Gtk.Adjustment(math.degrees(model.element_angle_offset), -1, 360, 1, 0, 0)
+        self.spin_element_angle = Gtk.SpinButton(adjustment=ad2, climb_rate=1, digits=0)
+        self.spin_element_angle.connect("value-changed", self.get_element_angle)
+
+        model.base_polygon_angle = 0
+        ad = Gtk.Adjustment(math.degrees(model.base_polygon_angle), -1, 360, 1, 0, 0)
+        self.spin_global_angle = Gtk.SpinButton(adjustment=ad, climb_rate=1, digits=0)
+        self.spin_global_angle.connect("value-changed", self.get_global_angle)
+
+        self.darea = Gtk.DrawingArea()
+        self.darea.connect("draw", self.draw)
+        self.darea.set_size_request(CANVAS_WIDTH, CANVAS_HEIGHT)
+
+        self.export_button = Gtk.Button(label="Export diagram")
+        self.export_button.connect("clicked", self.on_export_diagram)
+
+        controls = Gtk.VBox(spacing=10)
+
+        vbox = Gtk.VBox()
+        vbox.pack_start(Gtk.Label("Elements"), False, False, 0)
+        vbox.pack_start(self.spin_base_polygon_sides, False, False, 0)
+        controls.pack_start(vbox, False, False, 0)
+
+        vbox = Gtk.VBox()
+        vbox.pack_start(Gtk.Label("Element sides"), False, False, 0)
+        vbox.pack_start(self.spin_elem_sides, False, False, 0)
+        controls.pack_start(vbox, False, False, 0)
+
+        vbox = Gtk.VBox()
+        vbox.pack_start(Gtk.Label("Element radius"), False, False, 0)
+        vbox.pack_start(self.spin_element_radius, False, False, 0)
+        controls.pack_start(vbox, False, False, 0)
+
+        controls.pack_start(self.checkbox_radial_orientate, False, False, 0)
+
+        vbox = Gtk.VBox()
+        vbox.pack_start(Gtk.Label("Element angle"), False, False, 0)
+        vbox.pack_start(self.spin_element_angle, False, False, 0)
+        controls.pack_start(vbox, False, False, 0)
+
+        vbox = Gtk.VBox()
+        vbox.pack_start(Gtk.Label("Global angle"), False, False, 0)
+        vbox.pack_start(self.spin_global_angle, False, False, 0)
+        controls.pack_start(vbox, False, False, 0)
+
+        controls.pack_start(self.checkbox_base_polygon, False, False, 0)
+
+        vbox = Gtk.VBox()
+        vbox.pack_start(Gtk.Label("Base polygon radius"), False, False, 0)
+        vbox.pack_start(self.spin_base_polygon_radius, False, False, 0)
+        controls.pack_start(vbox, False, False, 0)
+
+        controls.pack_start(self.checkbox_elements, False, False, 0)
+        controls.pack_start(self.checkbox_stars, False, False, 0)
+        controls.pack_start(self.checkbox_labels, False, False, 0)
+        controls.pack_end(self.export_button, False, False, 0)
+
+        main_container = Gtk.HBox(spacing=10)
+        main_container.add(controls)
+        main_container.add(self.darea)
+
+        self.add(main_container)
+
+    def normalize_angle(self, angle):
+        if angle == 360:
+            angle = 0
+        elif angle == -1:
+            angle = 359
+
+        return angle
+
+    def get_base_polygon_sides(self, event):
+        self.model.base_polygon_sides = self.spin_base_polygon_sides.get_value_as_int()
+        self.darea.queue_draw()
+
+    def get_elem_sides(self, event):
+        self.model.element_sides = self.spin_elem_sides.get_value_as_int()
+        self.darea.queue_draw()
+
+    def get_element_radius(self, event):
+        self.model.element_radius = self.spin_element_radius.get_value_as_int()
+        self.darea.queue_draw()
+
+    def get_radial_orientate(self, event):
+        self.model.radial_orientate = self.checkbox_radial_orientate.get_active()
+        self.darea.queue_draw()
+
+    def get_base_polygon(self, event):
+        self.model.show_base_polygon = self.checkbox_base_polygon.get_active()
+        self.darea.queue_draw()
+
+    def get_base_polygon_radius(self, event):
+        self.model.base_polygon_radius = self.spin_base_polygon_radius.get_value_as_int()
+        self.darea.queue_draw()
+
+    def get_draw_elements(self, event):
+        self.model.show_elements = self.checkbox_elements.get_active()
+        self.darea.queue_draw()
+
+    def get_draw_stars(self, event):
+        self.model.show_stars = self.checkbox_stars.get_active()
+        self.darea.queue_draw()
+
+    def get_draw_labels(self, event):
+        self.model.show_labels = self.checkbox_labels.get_active()
+        self.darea.queue_draw()
+
+    def get_global_angle(self, event):
+        angle = self.normalize_angle(self.spin_global_angle.get_value_as_int())
+        self.spin_global_angle.set_value(angle)
+        self.model.base_polygon_angle = math.radians(angle)
+        self.darea.queue_draw()
+
+    def get_element_angle(self, event):
+        angle = self.normalize_angle(self.spin_element_angle.get_value_as_int())
+        self.spin_element_angle.set_value(angle)
+        self.model.element_angle_offset = math.radians(angle)
+        self.darea.queue_draw()
+
+    def on_export_diagram(self, event):
+        dialog = Gtk.FileChooserDialog("Export Diagram", self,
+                                       Gtk.FileChooserAction.SAVE,
+                                       (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                        Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+        dialog.set_property('do-overwrite-confirmation', True)
+
+        filter_svg = Gtk.FileFilter()
+        filter_svg.set_name("SVG files")
+        filter_svg.add_mime_type("image/svg+xml")
+        dialog.add_filter(filter_svg)
+
+        filter_png = Gtk.FileFilter()
+        filter_png.set_name("PNG files")
+        filter_png.add_mime_type("image/png")
+        dialog.add_filter(filter_png)
+
+        response = dialog.run()
+        if response == Gtk.ResponseType.OK:
+            filename = dialog.get_filename()
+            if filename.endswith(".svg"):
+                self.diagram.save_svg(filename[:-4])
+            elif filename.endswith(".png"):
+                self.diagram.save_png(filename[:-4])
+
+        dialog.destroy()
+
+    def draw(self, darea, cr):
+        self.diagram.draw(self.model)
+
+        src_surface = self.diagram.surface
+        cr.set_source_surface(src_surface, 0, 0)
+        cr.paint()
+
+
+class RadialSymmetryApp(Gtk.Application):
+
+    def __init__(self):
+        Gtk.Application.__init__(self,
+                                 application_id=APPLICATION_ID,
+                                 flags=Gio.ApplicationFlags.FLAGS_NONE)
+        self.connect("activate", self.activateCb)
+
+    def activateCb(self, app):
+        window = RadialSymmetryWindow()
+        app.add_window(window)
+        window.show_all()
+
+if __name__ == "__main__":
+    app = RadialSymmetryApp()
+    exit_status = app.run(sys.argv)
+    sys.exit(exit_status)
diff --git a/RadialSymmetryDiagram.py b/RadialSymmetryDiagram.py
new file mode 100755 (executable)
index 0000000..e6bc383
--- /dev/null
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+#
+# A study on radial symmetry based
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from math import *
+import colorsys
+import Diagram
+
+
+class RadialSymmetryModel():
+    def __init__(self, base_polygon_sides=6, base_polygon_radius=None,
+                 base_polygon_angle=0, show_base_polygon=False,
+                 element_sides=3, element_radius=None, element_angle_offset=0,
+                 radial_orientation=True, show_elements=True, show_stars=False,
+                 show_labels=False):
+        self.base_polygon_sides = base_polygon_sides
+        self.base_polygon_radius = base_polygon_radius
+        self.base_polygon_angle = base_polygon_angle
+        self.show_base_polygon = show_base_polygon
+        self.element_sides = element_sides
+        self.element_radius = element_radius
+        self.element_angle_offset = element_angle_offset
+        self.radial_orientate = radial_orientation
+        self.show_elements = show_elements
+        self.show_stars = show_stars
+        self.show_labels = show_labels
+
+
+class RadialSymmetryDiagram(Diagram.Diagram):
+
+    def __init__(self, width, height, background=[1, 1, 1]):
+        Diagram.Diagram.__init__(self, width, height, background)
+
+    def draw(self, model):
+        cx = self.width / 2.0
+        cy = self.height / 2.0
+
+        # internal radius
+        if model.base_polygon_radius is None:
+            model.base_polygon_radius = min(self.width, self.height) / 4.0
+
+        if model.element_radius is None:
+            model.element_radius = model.base_polygon_radius
+
+        self.clear()
+        self.draw_elements(cx, cy, model)
+
+    def draw_elements(self, cx, cy, model):
+        central_angle = 2 * pi / model.base_polygon_sides
+
+        base_polygon_orientation = central_angle + model.base_polygon_angle
+
+        verts = self.get_regular_polygon(cx, cy, model.base_polygon_sides, model.base_polygon_radius,
+                                         base_polygon_orientation)
+
+        if model.show_base_polygon:
+            self.draw_polygon(verts, [0, 0, 0])
+
+        for i, v in enumerate(verts[:]):
+            radial_orientation_angle = (i + 1) * central_angle
+            rotated_radial_orientation_angle = radial_orientation_angle + model.base_polygon_angle
+
+            # the element orientation may depend the base polygon rotation
+            if model.radial_orientate:
+                element_angle = rotated_radial_orientation_angle
+            else:
+                element_angle = 2 * pi / model.element_sides
+
+            element_angle += model.element_angle_offset
+
+            # the element color depends oly on the element index
+            a = self.normalized_angle_01(radial_orientation_angle)
+            color = colorsys.hsv_to_rgb(a, 1.0, 1.0)
+
+            self.draw_element(v[0], v[1], model, element_angle, color)
+
+            if model.show_labels:
+                ta = self.normalized_angle_01(rotated_radial_orientation_angle)
+                text = ("%.2f" % (ta * 360)).rstrip('0').rstrip('.')
+                color = colorsys.hsv_to_rgb(a, 1.0, 1.0)
+                self.draw_centered_text(v[0], v[1], text,
+                                        rotated_radial_orientation_angle, color)
+
+    def draw_element(self, x, y, model, theta, color=[0, 0, 0]):
+        if model.show_elements:
+            tverts = self.get_regular_polygon(x, y, model.element_sides, model.element_radius, theta)
+            self.draw_polygon(tverts, color)
+
+        if model.show_stars:
+            apothem = model.element_radius * cos(pi / model.element_sides)
+            apothem_angle = theta + pi / model.element_sides
+
+            sverts = self.get_regular_polygon(x, y, model.element_sides, apothem,
+                                              apothem_angle)
+            self.draw_star(x, y, sverts, color)
+
+
+if __name__ == '__main__':
+    h = RadialSymmetryDiagram(800, 600)
+    h.draw(RadialSymmetryModel(base_polygon_sides=10, element_sides=4, show_stars=True, show_labels=False))
+    h.draw(RadialSymmetryModel(base_polygon_sides=31, element_sides=4, show_stars=False, show_labels=False))
+    h.show()
+    h.save_svg("radial_symmetry_test")
+    h.save_png("radial_symmetry_test")
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..a6523f7
--- /dev/null
+++ b/TODO
@@ -0,0 +1 @@
+- Saving to SVG only saves a bitmap version of the diagram