<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>lib/celtic_knot/builder.rb</filename>
    </added>
    <added>
      <filename>lib/celtic_knot/direction.rb</filename>
    </added>
    <added>
      <filename>lib/celtic_knot/thread.rb</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -1,85 +1,81 @@
 module CelticKnot
   class Edge
+    def self.class_for(type)
+      case type
+      when ',' then NormalEdge
+      when '-' then IgnoredEdge
+      when '=' then ImperviousEdge
+      else raise ArgumentError, &quot;expected one of (',', '-', '=') for edge type&quot;
+      end
+    end
+
     attr_reader :n1, :n2
     attr_reader :type
     attr_reader :passes
 
-    def initialize(n1, n2, type)
-      @n1, @n2, @type = n1, n2, type
+    def initialize(n1, n2)
+      @n1, @n2 = n1, n2
 
-      n1.edges &lt;&lt; self
-      n2.edges &lt;&lt; self
+      n1.add_edge(self)
+      n2.add_edge(self)
 
-      @marked = []
+      @marked = {}
+      @intersections = 0
     end
 
-    def mark(n)
-      raise ArgumentError, &quot;node is not part of edge&quot; unless n1 == n || n2 == n
-      raise &quot;already marked #{n} for #{self}&quot; if @marked.include?(n)
-      @marked &lt;&lt; n
+    def mark(point, direction)
+      raise &quot;already marked #{point} on #{self}&quot; if @marked[[point, direction]]
+      mark_near(point, direction)
+      mark_opposite(point, direction)
+      @intersections += 1
     end
 
-    def marked?(n)
-      @marked.include?(n)
+    def marked?(point, direction)
+      @marked[[point, direction]]
     end
 
     def finished?
-      @marked.length == 2
+      @intersections == 2
     end
 
     def midpoint
-      @midpoint ||= n1 + (n2 - n1)/2.0
-    end
-
-    def virtual_midpoint(from, mode)
-      if normal?
-        return midpoint
-      elsif impervious?
-        direction = from.direction_to(midpoint)
-        return midpoint + direction.cross_right * 5 # FIXME: don't hard-code the cable width
-      elsif ignore?
-        direction = from.direction_to(midpoint)
-        distance = (midpoint - from).length
-        offset = (mode == :incoming ? -3 : 3) # FIXME: don't hardcode the cable width
-        return from + direction * (distance + offset)
-      end
+      @midpoint ||= Curves::Point.new((n1.x + n2.x)/2.0, (n1.y + n2.y)/2.0)
     end
 
-    def vector(direction, mode)
-      if normal?
-        result = direction.between(direction.cross_right).normalize
-        result = result.cross_right if mode == :incoming
-      elsif impervious?
-        result = mode == :outgoing ? direction : direction.inverse
-      elsif ignore?
-        result = direction.cross_right
-      end
-
-      return result
+    def length
+      (n1 - n2).length
     end
 
     def other(node)
       node == n1 ? n2 : n1
     end
 
-    def where_is(point)
-      point.y - n1.y - (point.x - n1.x) * ((n2.y - n1.y).to_f / (n2.x - n1.x))
+    def angle_to(edge, direction=Direction::CCW)
+      common = ([edge.n1, edge.n2] &amp; [n1, n2]).first
+      vector1 = other(common) - common
+      vector2 = edge.other(common) - common
+      result = vector1.angle(vector2)
+      result = 2 * Math::PI - result if direction.cw?
+      return result
+    end
+
+    # Returns a number from 0.0 (edge is the same as self) to 1.0
+    # (edge are 360 degrees rotated in the given direction). Two
+    # colinear edges will have a difference of 0.5 (180 degrees).
+    def difference(edge, direction=Direction::CCW)
+      angle_to(edge, direction) / (2 * Math::PI)
     end
 
     def normal?
-      type == &quot;,&quot;
+      false
     end
 
     def ignore?
-      type == &quot;-&quot;
+      false
     end
 
     def impervious?
-      type == &quot;=&quot;
-    end
-
-    def to_s
-      &quot;&lt;%p%s%p&gt;&quot; % [n1, type, n2]
+      false
     end
 
     def to_svg(options={})
@@ -94,5 +90,122 @@ module CelticKnot
       svg &lt;&lt; 'stroke=&quot;%s&quot; fill=&quot;none&quot; stroke-width=&quot;%s&quot; stroke-dasharray=&quot;%s&quot;' % [color, width, dasharray]
       svg &lt;&lt; &quot; /&gt;&quot;
     end
+
+    private
+
+      def mark_near(point, direction)
+        @marked[[point, direction]] = true
+      end
+  end
+
+
+  class NormalEdge &lt; Edge
+    # Return the &quot;virtual&quot; midpoint.
+    #
+    # This will be the natural midpoint of the edge. The reference, direction,
+    # and phase parameters are all ignored for normal edges.
+    def virtual_midpoint(reference, direction, phase)
+      midpoint
+    end
+
+    def vector(parallel, direction)
+      parallel.between(direction.cw? ? parallel.rotate_ccw : parallel.rotate_cw)
+    end
+
+    def normal?
+      true
+    end
+
+    def to_s
+      &quot;&lt;%s,%s&gt;&quot; % [n1, n2]
+    end
+
+    private
+
+      def mark_opposite(point, direction)
+        far = other(point)
+        @marked[[far, direction]] = true
+      end
+  end
+
+
+  class ImperviousEdge &lt; Edge
+    # Return the &quot;virtual&quot; midpoint.
+    #
+    # This will be a point offset some distance from the natural midpoint,
+    # perpendicular to the edge. In this case, the direction parameter is
+    # used to determine which side of the edge the point is offset from; if
+    # direction is Direction::CW (clockwise), it will be offset as though it
+    # were going to orbit in a clockwise direction around the reference node.
+    # Likewise, if the direction is Direction::CCW (counter-clockwise) it will
+    # be offset as thought it were going to orbit the reference node in a
+    # counter-clockwise direction. The phase parameter is ignored, in either
+    # case.
+    def virtual_midpoint(reference, direction, phase)
+      vector = midpoint - reference
+      vector = direction == Direction::CCW ? vector.rotate_cw : vector.rotate_ccw
+      return midpoint + vector.normalize * length/4 # FIXME: don't hard-code the cable width
+    end
+
+    def vector(parallel, direction)
+      parallel
+    end
+
+    def impervious?
+      true
+    end
+
+    def to_s
+      &quot;&lt;%s=%s&gt;&quot; % [n1, n2]
+    end
+
+    private
+
+      def mark_opposite(point, direction)
+        far = other(point)
+        @marked[[far, direction.opposite]] = true
+      end
+  end
+
+
+  class IgnoredEdge &lt; Edge
+    # Return the &quot;virtual&quot; midpoint.
+    #
+    # This will be a point offset some distance from the natural midpoint,
+    # colinear with the edge. In this case, the direction parameter is
+    # ignored, and the midpoint returned will always be the one nearest the
+    # reference node if phase is :enter, and the furthest if phase is :exit.
+    def virtual_midpoint(reference, direction, phase)
+      direction = midpoint - reference
+      distance = length/3
+      offset = phase == :enter ? -1 : 1 # FIXME: don't hardcode the cable width
+      return reference + direction.normalize * (distance + offset)
+    end
+
+    def vector(parallel, direction)
+      if direction.cw?
+        parallel.rotate_ccw
+      else
+        parallel.rotate_cw
+      end
+    end
+
+    def ignore?
+      true
+    end
+
+    def to_s
+      &quot;&lt;%s-%s&gt;&quot; % [n1, n2]
+    end
+
+    private
+
+      def mark_opposite(point, direction)
+      end
+
+      def mark_near(point, direction)
+        @marked[[point, direction]] = true
+        @marked[[point, direction.opposite]] = true
+      end
   end
 end</diff>
      <filename>lib/celtic_knot/edge.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,11 +1,7 @@
 require 'strscan'
 
-require 'celtic_knot/bezier'
 require 'celtic_knot/edge'
-require 'celtic_knot/knot'
-require 'celtic_knot/line'
 require 'celtic_knot/node'
-require 'celtic_knot/triangle'
 
 module CelticKnot
   class Graph
@@ -33,7 +29,7 @@ module CelticKnot
             scanner.skip(/\s*/)
             far = scanner.scan(/\w+/) or abort &quot;expected node id on #{line.inspect}&quot;
             p2 = nodes[far.downcase] or abort &quot;unknown point #{far.inspect}&quot;
-            Edge.new(p1, p2, type)
+            Edge.class_for(type).new(p1, p2)
             p1 = p2
           end
         end
@@ -51,38 +47,15 @@ module CelticKnot
     def construct_knot
       knot = Knot.new
 
-      near = nodes.first
-      edge = near.edges.first
-      far = edge.other(near)
-      pass = 0
-
-      loop do
-        if edge.finished?
-          near = nodes.detect { |n| n.edges.any? { |e| !e.finished? } }
-          break unless near
-          edge = near.edges.detect { |e| !e.finished? }
-          far = edge.other(near)
-          pass = 0
-        end
-
-        if edge.marked?(near)
-          far, near = near, edge.other(near)
-        end
-
-        edge.mark(near)
-
-        d = near.direction_to(far)
-        perp = d.cross_right
-
-        if far.edges.length == 1
-          near, edge = process_deadend(knot, edge, near, far, d, perp)
-        elsif far.edges.length == 2
-          near, edge = process_corner(knot, edge, near, far, d, perp)
+      nodes.each do |node|
+        if node.edges.empty?
+          encircle_node(knot, node)
         else
-          near, edge = process_intersection(knot, edge, near, far, d, perp)
+          node.edges.each do |edge|
+            next if edge.marked?(node, Direction::CCW)
+            plot_thread(knot, node, edge)
+          end
         end
-
-        far = edge.other(near)
       end
 
       return knot
@@ -100,90 +73,57 @@ module CelticKnot
 
     private
 
-      def process_deadend(knot, edge, near, far, d, perp)
-        lperp = perp.inverse
-        direction = far - edge.midpoint
-        distance = direction.length
-        curve_mid = far + direction
-
-        outgoing_vector = d.between(perp).normalize
-
-        points = [edge.midpoint, edge.midpoint + outgoing_vector * distance, curve_mid + perp * distance, curve_mid]
-        knot.add(edge, points, true)
+      def encircle_node(knot, node)
+        # trivial case for a node with no edges. just draw a circle around it.
+        raise NotImplementedError
+      end
 
-        incoming_vector = d.between(lperp).normalize
+      def plot_thread(knot, near, edge)
+        thread = knot.new_thread
 
-        points = [curve_mid, curve_mid + lperp * distance, edge.midpoint + incoming_vector * distance, edge.midpoint]
-        knot.add(edge, points, false)
+        # direction is either &quot;clockwise&quot; or &quot;counter-clockwise&quot;,
+        # and refers to the direction the cable would travel if it were to
+        # orbit the far node, originating at the midpoint of the current
+        # edge.
 
-        return [near, edge]
-      end
-
-      def process_corner(knot, edge, near, far, d, perp)
-        edge2 = far.edges_without(edge).first
-        process_corner_with_edges(knot, edge, edge2, near, far, d, perp)
-      end
+        direction = Direction::CCW
 
-      def process_intersection(knot, edge, near, far, d, perp)
-        edge2 = far.nearest_edge_to(edge)
-        process_corner_with_edges(knot, edge, edge2, near, far, d, perp)
-      end
+        # mid1 is the &quot;virtual&quot; midpoint of the current edge, as viewed
+        # from the given starting node, and travelling in the given direction
+        # around the far node.
 
-      def process_corner_with_edges(knot, edge, edge2, near, far, d, perp)
-        return process_deadend(knot, edge, near, far, d, perp) if edge2.nil?
+        mid1 = edge.virtual_midpoint(near, direction, :exit)
 
-        far2 = edge2.other(far)
-        d2 = far.direction_to(far2)
-        perp2 = d2.cross_right
+n = 0
+        loop do
+          far = edge.other(near)
+          parallel = far - near
+          vector = edge.vector(parallel, direction)
 
-        triangle = Triangle.new(near, far, far2) if d != d2 # the two edges are not colinear
+          break if thread.closes?(mid1, vector)
 
-        mid1 = edge.virtual_midpoint(near, :outgoing)
-        mid2 = edge2.virtual_midpoint(far, :incoming)
+n += 1
+puts &quot;%d: %s %s | %s %s | %s %s&quot; % [n, near, edge, mid1, vector, parallel, direction]
+          thread.add_connection(mid1, vector)
 
-        outgoing_vector = edge.vector(d, :outgoing)
-        incoming_vector = edge2.vector(d2, :incoming)
+          edge.mark(near, direction)
 
-        if triangle &amp;&amp; triangle.contains?(mid1 + outgoing_vector*0.01)
-          # we're crossing a concave angle
-          l1 = Line.new(mid1, mid1 + outgoing_vector)
-          l2 = Line.new(mid2, mid2 + incoming_vector)
+          edge2 = far.nearest_edge_to(edge, direction) || edge
+          mid2 = edge2.virtual_midpoint(far, direction, :enter)
 
-          pmid = l1.intersect(l2) || mid1.between(mid2)
+          # if the edge is being ignored, then we reverse direction,
+          # keeping all else the same. This has the effect of just
+          # passing through the edge.
 
-          curve = Bezier.new([mid1, pmid, pmid, mid2])
-          over, under = curve.split(0.5)
+          if edge2.ignore?
+            near = edge2.other(far)
+          else
+            near = far
+            direction = direction.opposite if edge2.normal?
+          end
 
-          knot.add(edge, over.controls, true)
-          knot.add(edge2, under.controls, false)
-        else
-          # we're crossing a convex angle (outer corner)
-          base = mid1.between(mid2) # point between midpoints
-
-          # distance from the base to where the two curve segments will meet
-          # FIXME: for best results, this should probably take into account the tangents
-          # at each midpoint; otherwise, loops that circle a single node (due to ignored
-          # edges) will be very flat on one side.
-
-          midpoint_separation = (mid1 - mid2).length
-          max_separation = (far - mid1).length + (mid2 - far).length
-          ratio = (midpoint_separation / max_separation) ** 2
-          distance = (max_separation - 5) * (1 - ratio) + 5 # FIXME: don't hard-code cable width
-
-          e2e = mid1.direction_to(mid2).normalize
-          meetup = base + e2e.cross_right * distance
-          direction = base.direction_to(meetup)
-
-          points = [mid1, mid1 + outgoing_vector * distance * 0.5,
-            meetup + direction.cross_right.normalize * distance * 0.5, meetup]
-          knot.add(edge, points, true)
-          
-          points = [meetup, meetup + direction.cross_left.normalize * distance * 0.5,
-            mid2 + incoming_vector * distance * 0.5, mid2]
-          knot.add(edge2, points, false)
+          edge, mid1 = edge2, mid2
         end
-
-        return [far, edge2]
       end
   end
 end</diff>
      <filename>lib/celtic_knot/graph.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,21 +1,17 @@
-require 'celtic_knot/knot_bezier'
-require 'celtic_knot/path'
+require 'celtic_knot/thread'
 
 module CelticKnot
   class Knot
-    attr_reader :curves
+    attr_reader :threads
 
     def initialize
-      @curves = []
-      @marks = {}
+      @threads = []
     end
 
-    def add(edge, points, over)
-      curves &lt;&lt; KnotBezier.new(points, over)
-    end
-
-    def mark(id, point)
-      @marks[id] = point
+    def new_thread
+      thread = Thread.new
+      @threads &lt;&lt; thread
+      return thread
     end
 
     def to_svg(options={})</diff>
      <filename>lib/celtic_knot/knot.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,11 +1,12 @@
-require 'celtic_knot/point'
+require 'curves/point'
+require 'celtic_knot/direction'
 
 module CelticKnot
-  class Node &lt; Point
+  class Node &lt; Curves::Point
     attr_reader :edges
 
-    def initialize(x, y)
-      super(x,y)
+    def initialize(x, y, z=0)
+      super(x,y,z)
       @edges = []
     end
 
@@ -13,23 +14,31 @@ module CelticKnot
       edges.reject { |e| e == edge }
     end
 
-    def nearest_edge_to(edge)
-      smallest_angle = 2*Math::PI
-      nearest_edge = nil
-
-      vector = edge.other(self).direction_to(self)
-
-      list = edges_without(edge)
-      list.each do |e2|
-        vector2 = self.direction_to(e2.other(self))
-        angle = vector.angle(vector2)
-        if angle &lt; smallest_angle
-          smallest_angle = angle
-          nearest_edge = e2
-        end
+    def sort_edges!
+      if edges.length &gt; 2
+        first = edges.first
+        angles = edges.inject({}) { |h,e| h[e] = first.angle_to(e); h }
+        edges.sort! { |a,b| angles[a] &lt;=&gt; angles[b] }
       end
+    end
+
+    def add_edge(edge)
+      edges &lt;&lt; edge
+      sort_edges!
+      return edge
+    end
 
-      return nearest_edge
+    # direction must be one of:
+    # * Direction::CW -- clockwise
+    # * Direction::CCW -- counter-clockwise
+    #
+    # assumes that the edges are in sorted order
+    def nearest_edge_to(edge, direction)
+      idx = edges.index(edge) or raise &quot;edge #{e} is not attached to #{self}&quot;
+      nearest = direction == Direction::CW ? idx-1 : idx+1
+      nearest = edges.length-1 if nearest &lt; 0
+      nearest = 0 if nearest &gt;= edges.length
+      return edges[nearest]
     end
 
     def to_svg(options={})</diff>
      <filename>lib/celtic_knot/node.rb</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>lib/celtic_knot/bezier.rb</filename>
    </removed>
    <removed>
      <filename>lib/celtic_knot/knot_bezier.rb</filename>
    </removed>
    <removed>
      <filename>lib/celtic_knot/line.rb</filename>
    </removed>
    <removed>
      <filename>lib/celtic_knot/path.rb</filename>
    </removed>
    <removed>
      <filename>lib/celtic_knot/point.rb</filename>
    </removed>
    <removed>
      <filename>lib/celtic_knot/triangle.rb</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>bdf4e21f68c315d9fe0878f0025649a3c92640e3</id>
    </parent>
  </parents>
  <author>
    <name>Jamis Buck</name>
    <email>jamis@37signals.com</email>
  </author>
  <url>http://github.com/jamis/celtic_knot/commit/825d93ff04f5064f7222704271f3abc1250f3ef7</url>
  <id>825d93ff04f5064f7222704271f3abc1250f3ef7</id>
  <committed-date>2009-06-06T22:19:31-07:00</committed-date>
  <authored-date>2009-06-06T22:19:31-07:00</authored-date>
  <message>follow individual threads

This lets us color separate threads differently</message>
  <tree>3de605a758982d80b88a408128bdadb39b949f88</tree>
  <committer>
    <name>Jamis Buck</name>
    <email>jamis@37signals.com</email>
  </committer>
</commit>
