From 3bd63f6ff461b96743829d8fe62731000b9b74ed Mon Sep 17 00:00:00 2001
From: Walter Bender <walter@sugarlabs.org>
Date: Wed, 13 Oct 2010 12:41:39 -0400
Subject: [PATCH] a more succint approach to the spiral morph

---
 src/jarabe/desktop/favoriteslayout.py |  182 +++++++++++++++++++++++++++------
 1 files changed, 149 insertions(+), 33 deletions(-)

diff --git a/src/jarabe/desktop/favoriteslayout.py b/src/jarabe/desktop/favoriteslayout.py
index 85e1b59..8bc0f1f 100644
--- a/src/jarabe/desktop/favoriteslayout.py
+++ b/src/jarabe/desktop/favoriteslayout.py
@@ -1,4 +1,5 @@
 # Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2010 Sugar Labs
 #
 # 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
@@ -181,18 +182,29 @@ class RandomLayout(FavoritesLayout):
     def allow_dnd(self):
         return True
 
+
 _MINIMUM_RADIUS = style.XLARGE_ICON_SIZE / 2 + style.DEFAULT_SPACING + \
         style.STANDARD_ICON_SIZE * 2
 _MAXIMUM_RADIUS = (gtk.gdk.screen_height() - style.GRID_CELL_SIZE) / 2 - \
         style.STANDARD_ICON_SIZE - style.DEFAULT_SPACING
-
-class RingLayout(FavoritesLayout):
+_INTERMEDIATE_C = (style.STANDARD_ICON_SIZE + style.SMALL_ICON_SIZE) / 2
+_INTERMEDIATE_A = (style.STANDARD_ICON_SIZE * 2 + _INTERMEDIATE_C) / 3
+_INTERMEDIATE_E = (_INTERMEDIATE_C + style.SMALL_ICON_SIZE * 2) / 3
+_INTERMEDIATE_B = (_INTERMEDIATE_A + _INTERMEDIATE_C) / 2
+_INTERMEDIATE_D = (_INTERMEDIATE_C + _INTERMEDIATE_E) / 2
+_ICON_SIZES = [style.MEDIUM_ICON_SIZE, style.STANDARD_ICON_SIZE,
+               _INTERMEDIATE_A, _INTERMEDIATE_B, _INTERMEDIATE_C,
+               _INTERMEDIATE_D, _INTERMEDIATE_E, style.SMALL_ICON_SIZE]
+_ICON_SPACING_FACTORS = [1.5, 1.4, 1.3, 1.2, 1.15, 1.1, 1.05, 1.0]
+
+
+class BasicRingLayout(FavoritesLayout):
     """Lay out icons in a ring around the XO man."""
 
-    __gtype_name__ = 'RingLayout'
+    __gtype_name__ = 'BasicRingLayout'
     icon_name = 'view-radial'
     """Name of icon used in home view dropdown palette."""
-    key = 'ring-layout'
+    key = 'basic-ring-layout'
     """String used in profile to represent this view."""
     # TRANS: label for the ring layout in the favorites view
     palette_name = _('Ring')
@@ -221,31 +233,36 @@ class RingLayout(FavoritesLayout):
             self._locked_children[child] = (x, y)
 
     def _calculate_radius_and_icon_size(self, children_count):
-        # what's the radius required without downscaling?
-        distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING
-        icon_size = style.STANDARD_ICON_SIZE
-        # circumference is 2*pi*r; we want this to be at least
-        # 'children_count * distance'
-        radius = children_count * distance / (2 * math.pi)
-        # limit computed radius to reasonable bounds.
-        radius = max(radius, _MINIMUM_RADIUS)
-        radius = min(radius, _MAXIMUM_RADIUS)
-        # recompute icon size from limited radius
-        if children_count > 0:
-            icon_size = (2 * math.pi * radius / children_count) \
-                        - style.DEFAULT_SPACING
-        # limit adjusted icon size.
-        icon_size = max(icon_size, style.SMALL_ICON_SIZE)
-        icon_size = min(icon_size, style.MEDIUM_ICON_SIZE)
+        """ Adjust the ring radius and icon size as needed. """
+        # Begin by increasing the radius.
+        distance = style.MEDIUM_ICON_SIZE + style.DEFAULT_SPACING * \
+            _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.MEDIUM_ICON_SIZE)]
+        radius = max(children_count * distance / (2 * math.pi), _MINIMUM_RADIUS)
+        if radius < _MAXIMUM_RADIUS:
+            return radius, style.MEDIUM_ICON_SIZE
+
+        # Continue by shrinking the icon size to STANDARD_ICON_SIZE.
+        radius = _MAXIMUM_RADIUS
+        distance = radius * (2 * math.pi) / children_count
+        icon_size = int(distance - style.DEFAULT_SPACING * \
+            _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.STANDARD_ICON_SIZE)])
+        if icon_size >= style.STANDARD_ICON_SIZE:
+            return radius, icon_size
+
+        # Continue by shrinking the icon size to SMALL_ICON_SIZE.
+        icon_size = max(int(distance - style.DEFAULT_SPACING * \
+                            _ICON_SPACING_FACTORS[_ICON_SIZES.index(
+                    style.SMALL_ICON_SIZE)]), style.SMALL_ICON_SIZE)
         return radius, icon_size
 
-    def _calculate_position(self, radius, icon_size, index, children_count,
+    def _calculate_position(self, radius, icon_size, icon_index, children_count,
                             sin=math.sin, cos=math.cos):
+        """ Calculate an icon position on a circle. """
         width, height = self.box.get_allocation()
-        angle = index * (2 * math.pi / children_count) - math.pi / 2
+        angle = icon_index * (2 * math.pi / children_count) - math.pi / 2
         x = radius * cos(angle) + (width - icon_size) / 2
-        y = radius * sin(angle) + (height - icon_size -
-                                   (style.GRID_CELL_SIZE/2) ) / 2
+        y = radius * sin(angle) + (height - icon_size - \
+                                       (style.GRID_CELL_SIZE / 2)) / 2
         return x, y
 
     def _get_children_in_ring(self):
@@ -294,6 +311,104 @@ class RingLayout(FavoritesLayout):
         else:
             return 0
 
+
+_MIMIMUM_RADIUS_ENCROACHMENT = 0.75
+_INITIAL_ANGLE = math.pi
+_SPIRAL_SPACING_FACTORS = [1.5, 1.5, 1.5, 1.4, 1.35, 1.3, 1.25, 1.2]
+
+
+class RingLayout(BasicRingLayout):
+    """ Variation of Basic Ring that morphs into a spiral as
+    the number of icons increases beyond the capacity of the
+    STANDARD_ICON_SIZE. """
+
+    __gtype_name__ = 'RingLayout'
+    icon_name = 'view-radial'
+    """Name of icon used in home view dropdown palette."""
+    key = 'ring-layout'
+    """String used in profile to represent this view."""
+
+    def __init__(self):
+        BasicRingLayout.__init__(self)
+        self._locked_children = {}
+        self._spiral_mode = False
+
+    def _calculate_radius_and_icon_size(self, children_count):
+        """ Adjust the ring or spiral radius and icon size as needed. """
+        self._spiral_mode = False
+        # Begin by increasing the radius.
+        distance = style.MEDIUM_ICON_SIZE + style.DEFAULT_SPACING * \
+            _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.MEDIUM_ICON_SIZE)]
+        radius = max(children_count * distance / (2 * math.pi), _MINIMUM_RADIUS)
+        if radius < _MAXIMUM_RADIUS:
+            return radius, style.MEDIUM_ICON_SIZE
+
+        # Continue by shrinking the icon size.
+        radius = _MAXIMUM_RADIUS
+        distance = radius * (2 * math.pi) / children_count
+        icon_size = int(distance - style.DEFAULT_SPACING * \
+            _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.STANDARD_ICON_SIZE)])
+        if icon_size >= style.STANDARD_ICON_SIZE:
+            return radius, icon_size
+
+        # Finally, switch to a spiral.
+        self._spiral_mode = True
+        icon_size = style.STANDARD_ICON_SIZE
+        angle, radius = self._calculate_angle_and_radius(children_count,
+                                                         icon_size)
+        while radius > _MAXIMUM_RADIUS:
+            i = _ICON_SIZES.index(icon_size)
+            if i < len(_ICON_SIZES) - 1:
+                icon_size = _ICON_SIZES[i + 1]
+                angle, radius = self._calculate_angle_and_radius(
+                    children_count, icon_size)
+            else:
+                break
+        return radius, icon_size
+
+    def _calculate_position(self, radius, icon_size, icon_index, children_count,
+                            sin=math.sin, cos=math.cos):
+        """ Calculate an icon position on a circle or a spiral. """
+        width, height = self.box.get_allocation()
+        if self._spiral_mode:
+            min_width_, box_width = self.box.get_width_request()
+            min_height_, box_height = self.box.get_height_request(box_width)
+            angle, radius = self._calculate_angle_and_radius(icon_index,
+                                                             icon_size)
+            x, y = self._convert_from_polar_to_cartesian(angle, radius,
+                                                         icon_size,
+                                                         width, height)
+        else:
+            angle = icon_index * (2 * math.pi / children_count) - math.pi / 2
+            x = radius * cos(angle) + (width - icon_size) / 2
+            y = radius * sin(angle) + (height - icon_size - \
+                                       (style.GRID_CELL_SIZE / 2)) / 2
+        return x, y
+
+    def _convert_from_polar_to_cartesian(self, angle, radius, icon_size, width,
+                                         height):
+        """ Convert angle, radius to x, y """
+        x = int(math.sin(angle) * radius)
+        y = int(math.cos(angle) * radius)
+        x = - x + (width - icon_size) / 2
+        y = y + (height - icon_size - (style.GRID_CELL_SIZE / 2)) / 2
+        return x, y
+
+    def _calculate_angle_and_radius(self, icon_count, icon_size):
+        """ Based on icon_count and icon_size, calculate radius and angle. """
+        spiral_spacing = _SPIRAL_SPACING_FACTORS[_ICON_SIZES.index(icon_size)]
+        icon_spacing = icon_size + style.DEFAULT_SPACING * \
+            _ICON_SPACING_FACTORS[_ICON_SIZES.index(icon_size)]
+        angle = _INITIAL_ANGLE
+        radius = _MINIMUM_RADIUS - (icon_size * _MIMIMUM_RADIUS_ENCROACHMENT)
+        for i in range(icon_count):
+            circumference = radius * 2 * math.pi
+            n = circumference / icon_spacing
+            angle += (2 * math.pi / n)
+            radius += (float(icon_spacing) * spiral_spacing / n)
+        return angle, radius
+
+
 _SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75
 """Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced."""
 
@@ -319,7 +434,7 @@ This is the golden angle: http://en.wikipedia.org/wiki/Golden_angle
 Calculation: math.radians(360) / ( _GOLDEN_RATIO * _GOLDEN_RATIO )
 """
 
-class SunflowerLayout(RingLayout):
+class SunflowerLayout(BasicRingLayout):
     """Spiral layout based on Fibonacci ratio in phyllotaxis.
 
     See http://algorithmicbotany.org/papers/abop/abop-ch4.pdf
@@ -338,7 +453,7 @@ class SunflowerLayout(RingLayout):
     """String used to identify this layout in home view dropdown palette."""
 
     def __init__(self):
-        RingLayout.__init__(self)
+        BasicRingLayout.__init__(self)
         self.skipped_indices = []
 
     def _calculate_radius_and_icon_size(self, children_count):
@@ -389,7 +504,7 @@ class SunflowerLayout(RingLayout):
 
             return x, y
 
-class BoxLayout(RingLayout):
+class BoxLayout(BasicRingLayout):
     """Lay out icons in a square around the XO man."""
 
     __gtype_name__ = 'BoxLayout'
@@ -405,7 +520,7 @@ class BoxLayout(RingLayout):
     """String used to identify this layout in home view dropdown palette."""
 
     def __init__(self):
-        RingLayout.__init__(self)
+        BasicRingLayout.__init__(self)
 
     def _calculate_position(self, radius, icon_size, index, children_count,
                             sin=None, cos=None):
@@ -426,11 +541,11 @@ class BoxLayout(RingLayout):
         cos = lambda r: cos_d(math.degrees(r))
         sin = lambda r: cos_d(math.degrees(r) - 90)
 
-        return RingLayout._calculate_position\
+        return BasicRingLayout._calculate_position\
                (self, radius, icon_size, index, children_count,
                 sin=sin, cos=cos)
 
-class TriangleLayout(RingLayout):
+class TriangleLayout(BasicRingLayout):
     """Lay out icons in a triangle around the XO man."""
 
     __gtype_name__ = 'TriangleLayout'
@@ -446,13 +561,14 @@ class TriangleLayout(RingLayout):
     """String used to identify this layout in home view dropdown palette."""
 
     def __init__(self):
-        RingLayout.__init__(self)
+        BasicRingLayout.__init__(self)
 
     def _calculate_radius_and_icon_size(self, children_count):
         # use slightly larger minimum radius than parent, because sides
         # of triangle come awful close to the center.
         radius, icon_size = \
-            RingLayout._calculate_radius_and_icon_size(self, children_count)
+            BasicRingLayout._calculate_radius_and_icon_size(self,
+                                                            children_count)
         return max(radius, _MINIMUM_RADIUS + style.MEDIUM_ICON_SIZE), icon_size
 
     def _calculate_position(self, radius, icon_size, index, children_count,
@@ -483,6 +599,6 @@ class TriangleLayout(RingLayout):
         cos = lambda r: cos_d(math.degrees(r))
         sin = lambda r: sin_d(math.degrees(r))
 
-        return RingLayout._calculate_position\
+        return BasicRingLayout._calculate_position\
                (self, radius, icon_size, index, children_count,
                 sin=sin, cos=cos)
-- 
1.7.1

