#!/usr/bin/ruby # Draw square, hexagonal, and triangle grid illustrations # amitp@cs.stanford.edu # 8 Jan 2006 SQRT_3_2 = Math.sqrt(3)/2 # Used in triangle and hexagon math class Shape def slope(edge) v1, v2 = endpoints(edge) x1, y1 = vertex_to_world(1.0, v1) x2, y2 = vertex_to_world(1.0, v2) distance = Math.sqrt((x1-x2)**2 + (y1-y2)**2) [(x2-x1)/distance, (y2-y1)/distance] end end class Triangle < Shape def faces(m, n) [[m, n, :L], [m, n, :R]] end def corners(face) m, n, side = face case side when :L [[m, n+1], [m+1, n], [m, n]] when :R [[m+1, n+1], [m+1, n], [m, n+1]] end end def endpoints(edge) p, q, side = edge case side when :W [[p, q+1], [p, q]] when :E [[p+1, q], [p, q+1]] when :S [[p+1, q], [p, q]] end end def vertex_to_world(scale, corner) i, j = corner x = (i*1.0*scale + j*0.5*scale)/SQRT_3_2 y = j*scale [x, y] end end class Square < Shape def faces(m, n) [[m, n]] end def corners(face) m, n = face [[m+1, n+1], [m+1, n], [m, n], [m, n+1]] end def endpoints(edge) p, q, side = edge case side when :S [[p+1, q], [p, q]] when :W [[p, q+1], [p, q]] end end def vertex_to_world(scale, corner) i, j = corner [i*scale, j*scale] end end class Rhombus < Square def vertex_to_world(scale, corner) i, j = corner x = (i*1.0*scale + j*0.5*scale)/SQRT_3_2 y = (j+1)*scale [x, y] end end class Hexagon < Shape def initialize(edge_bend=1.0) @edge_bend = edge_bend # 0.0 for squares, 1.0 for hexagons end def faces(m, n) [[m, n]] end def corners(face) m, n = face [[m+1, n, :L], [m, n, :R], [m+1, n-1, :L], [m-1, n, :R], [m, n, :L], [m-1, n+1, :R]] end def endpoints(edge) p, q, side = edge case side when :N [[p+1, q, :L], [p-1, q+1, :R]] when :E [[p, q, :R], [p+1, q, :L]] when :W [[p-1, q+1, :R], [p, q, :L]] end end def vertex_to_world(scale, corner) i, j, side = corner x = scale*SQRT_3_2*i y = scale*1.0*0.5*(j*2 + i) if side == :R x = x + scale*(0.75+0.25*@edge_bend)/SQRT_3_2 end [x, y] end end class Canvas attr :width attr :height def initialize(width, height, grid) @width = width @height = height @grid = grid end def transform(x, y) x = 10 + x y = @height - 10 - y [x, y] end def to_svg output = '' output << '' output << '" output << %{ } output << @grid.to_svg(self) output << '' output end end class Grid attr_accessor :highlight_faces attr_accessor :highlight_edges attr_accessor :highlight_vertices attr_accessor :annotate_faces attr_accessor :annotate_edges attr_accessor :annotate_vertices def initialize(shape, scale) @shape = shape @scale = scale @faces = [] @highlight_faces = [] @highlight_edges = [] @highlight_vertices = [] @annotate_faces = {} @annotate_edges = {} @annotate_vertices = {} end def add_faces(*faces) faces.each { |face| @faces << face } end def add_facearray(m_list, n_list) m_list.each { |m| n_list.each { |n| @shape.faces(m, n).each { |face| add_faces(face) } } } end def vertex_midpoint(vertices) # Calculate the centroid of a set of vertices total_x, total_y, count = 0, 0, 0 vertices.each { |vertex| x, y = @shape.vertex_to_world(@scale, vertex) total_x += x total_y += y count += 1 } x = (total_x / count) y = (total_y / count) [x, y] end def to_svg(canvas) output = '' output << '' @faces.each { |face| if @highlight_faces.include?(face) color = @highlight_faces[face] output << "" end output << '' if @highlight_faces.include?(face) output << '' end } output << '' @highlight_edges.each { |edge, color| c = 0.2 # cut off this much from each end v1, v2 = @shape.endpoints(edge) x1, y1 = @shape.vertex_to_world(@scale, v1) x1, y1 = canvas.transform(x1, y1) x2, y2 = @shape.vertex_to_world(@scale, v2) x2, y2 = canvas.transform(x2, y2) dx, dy = x2-x1, y2-y1 output << "" } @highlight_vertices.each { |vertex, color| x, y = @shape.vertex_to_world(@scale, vertex) x, y = canvas.transform(x, y) output << "" } @annotate_faces.each { |face, text| x, y = vertex_midpoint(@shape.corners(face)) x, y = canvas.transform(x, y) output << "#{text}" } @annotate_edges.each { |edge, text| x, y = vertex_midpoint(@shape.endpoints(edge)) # We want to nudge this to the right to get the text off of the # edge. Get the slope, rotate it 90 degrees, force it to not # point left (by reflecting if needed), and then nudge. TODO: # ideally we'd also nudge towards the center of the face that # the text lies in. dx, dy = @shape.slope(edge) dx, dy = dy, -dx # rotate 90 degrees if dx < 0 dx, dy = -dx, -dy # reflect if we're pointing left end x += dx*@scale/15 y += dy*@scale/15 align = "" if dx == 0 # Special case: if the edge is horizontal, center the text # along the edge. TODO: can we extend this to other cases? align = " text-anchor=\"middle\"" end x, y = canvas.transform(x, y) output << "#{text}" } @annotate_vertices.each { |vertex, text| total_x, total_y, count = 0 x, y = @shape.vertex_to_world(@scale, vertex) # We will nudge this up and to the right x += @scale/10 y += @scale/20 x, y = canvas.transform(x, y) output << "#{text}" } output end end class Diagrams def shape_grid(shape, max) grid = Grid.new(shape, 20) grid.add_facearray(0...max, 0...max) h = 5*max/9 grid.highlight_faces = { [h, h] => "red", [h, h, :L] => "red" } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_grid(max=9) shape_grid(Square.new, max) end def rhombus_grid(max=9) shape_grid(Rhombus.new, max) end def hexagon_grid(max=9, edge_bend=1.0) shape_grid(Hexagon.new(edge_bend), max) end def triangle_grid(max=9) shape_grid(Triangle.new, max) end def triangle_grid_small # We want to highlight two triangles to show how they are related # to the rhombus grid = Grid.new(Triangle.new, 20) grid.add_facearray(0...5, 0...5) grid.highlight_faces = { [2, 2, :L] => "red", [2, 2, :R] => "red" } canvas = Canvas.new(400, 300, grid) canvas.to_svg end #################### def square_grid_face_coordinates grid = Grid.new(Square.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "red", } grid.annotate_faces = { [0, 0] => "0,0", [1, 0] => "1,0", [0, 1] => "0,1", [1, 1] => "1,1", [0, 2] => "0,2", [2, 0] => "2,0", [1, 2] => "1,2", [2, 1] => "2,1", [2, 2] => "2,2", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_grid_vertex_coordinates grid = Grid.new(Square.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "red", [2, 1] => "gray", [1, 2] => "gray", [2, 2] => "gray", } grid.highlight_vertices = { [1, 1] => "red", [2, 1] => "black", [1, 2] => "black", [2, 2] => "black", } grid.annotate_vertices = { [1, 1] => "1,1", [2, 1] => "2,1", [1, 2] => "1,2", [2, 2] => "2,2", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_grid_edge_coordinates grid = Grid.new(Square.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "red", [1, 2] => "gray", [2, 1] => "gray", } grid.highlight_edges = { [1, 1, :S] => "red", [1, 1, :W] => "red", [1, 2, :S] => "black", [2, 1, :W] => "black", } grid.annotate_edges = { [1, 1, :S] => "1,1 S", [1, 1, :W] => "1,1 W", [1, 2, :S] => "1,2 S", [2, 1, :W] => "2,1 W", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end #################### def square_rel_face_face grid = Grid.new(Square.new, 50) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "gray", [1, 0] => "red", [2, 1] => "red", [1, 2] => "red", [0, 1] => "red", } grid.annotate_faces = { [1, 1] => "A", [1, 0] => "B", [2, 1] => "B", [1, 2] => "B", [0, 1] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_face_edge grid = Grid.new(Square.new, 50) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "gray", } grid.highlight_edges = { [1, 1, :W] => "red", [1, 1, :S] => "red", [2, 1, :W] => "red", [1, 2, :S] => "red", } grid.annotate_faces = { [1, 1] => "A", [1, 0] => "B", [2, 1] => "B", [1, 2] => "B", [0, 1] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_face_vertex grid = Grid.new(Square.new, 50) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "gray", } grid.highlight_vertices = { [1, 1] => "red", [1, 2] => "red", [2, 1] => "red", [2, 2] => "red", } grid.annotate_faces = { [1, 1] => "A", [0, 0] => "B", [2, 0] => "B", [0, 2] => "B", [2, 2] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_edge_face grid = Grid.new(Square.new, 50) grid.add_facearray(0...3, 0...2) grid.highlight_edges = { [1, 1, :S] => "black", } grid.highlight_faces = { [1, 1] => "red", [1, 0] => "red", } grid.annotate_edges = { [1, 1, :S] => "A", } grid.annotate_faces = { [1, 1] => "B", [1, 0] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_edge_edge grid = Grid.new(Square.new, 50) grid.add_facearray(0...3, 0...2) grid.highlight_edges = { [1, 1, :S] => "black", [0, 1, :S] => "red", [2, 1, :S] => "red", } grid.annotate_edges = { [1, 1, :S] => "A", [0, 1, :S] => "B", [2, 1, :S] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_edge_vertex grid = Grid.new(Square.new, 50) grid.add_facearray(0...3, 0...2) grid.highlight_edges = { [1, 1, :S] => "black", } grid.highlight_vertices = { [1, 1] => "red", [2, 1] => "red", } grid.annotate_edges = { [1, 1, :S] => "A", [0, 1, :S] => "B", [2, 1, :S] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_vertex_face grid = Grid.new(Square.new, 50) grid.add_facearray(0...2, 0...2) grid.highlight_vertices = { [1, 1] => "black", } grid.highlight_faces = { [0, 0] => "red", [1, 1] => "red", [0, 1] => "red", [1, 0] => "red", } grid.annotate_vertices = { [1, 1] => "A", } grid.annotate_faces = { [0, 0] => "B", [1, 1] => "B", [0, 1] => "B", [1, 0] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_vertex_edge grid = Grid.new(Square.new, 50) grid.add_facearray(0...2, 0...2) grid.highlight_vertices = { [1, 1] => "black", } grid.highlight_edges = { [1, 0, :W] => "red", [1, 1, :W] => "red", [0, 1, :S] => "red", [1, 1, :S] => "red", } grid.annotate_vertices = { [1, 1] => "A", } grid.annotate_edges = { [1, 0, :W] => "B", [1, 1, :W] => "B", [0, 1, :S] => "B", [1, 1, :S] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def square_rel_vertex_vertex grid = Grid.new(Square.new, 50) grid.add_facearray(0...2, 0...2) grid.highlight_vertices = { [1, 1] => "black", [0, 1] => "red", [2, 1] => "red", [1, 0] => "red", [1, 2] => "red", } grid.annotate_vertices = { [1, 1] => "A", [0, 1] => "B", [2, 1] => "B", [1, 0] => "B", [1, 2] => "B", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end #################### def hexagon_grid_face_coordinates grid = Grid.new(Hexagon.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "red", } grid.annotate_faces = { [0, 0] => "0,0", [1, 0] => "1,0", [0, 1] => "0,1", [1, 1] => "1,1", [0, 2] => "0,2", [2, 0] => "2,0", [1, 2] => "1,2", [2, 1] => "2,1", [2, 2] => "2,2", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def hexagon_grid_vertex_coordinates grid = Grid.new(Hexagon.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "red", [2, 1] => "gray", [2, 0] => "gray", [0, 2] => "gray", [0, 1] => "gray", } grid.highlight_vertices = { [1, 1, :L] => "red", [1, 1, :R] => "red", [2, 1, :L] => "black", [2, 0, :L] => "black", [0, 2, :R] => "black", [0, 1, :R] => "black", } grid.annotate_vertices = { [1, 1, :L] => "1,1 L", [1, 1, :R] => "1,1 R", [2, 1, :L] => "2,1 L", [2, 0, :L] => "2,0 L", [0, 2, :R] => "0,2 R", [0, 1, :R] => "0,1 R", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def hexagon_grid_edge_coordinates grid = Grid.new(Hexagon.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1] => "red", [0, 1] => "gray", [1, 0] => "gray", [2, 0] => "gray", } grid.highlight_edges = { [1, 1, :N] => "red", [1, 1, :W] => "red", [1, 1, :E] => "red", [0, 1, :E] => "black", [1, 0, :N] => "black", [2, 0, :W] => "black", } grid.annotate_edges = { [1, 1, :N] => "1,1 N", [1, 1, :W] => "1,1 W", [1, 1, :E] => "1,1 E", [0, 1, :E] => "0,1 E", [1, 0, :N] => "1,0 N", [2, 0, :W] => "2,0 W", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def hex_grid_metrics grid = Grid.new(Hexagon.new, 100) grid.add_facearray(0..1, 0..1) canvas = Canvas.new(400, 300, grid) # TODO: this does not include the lines we want; I'm adding them # manually in Inkscape. :-( canvas.to_svg end #################### def triangle_grid_face_coordinates grid = Grid.new(Triangle.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1, :L] => "red", [1, 1, :R] => "red", } grid.annotate_faces = { [0, 0, :L] => "0,0 L", [1, 0, :L] => "1,0 L", [0, 1, :L] => "0,1 L", [1, 1, :L] => "1,1 L", [0, 2, :L] => "0,2 L", [2, 0, :L] => "2,0 L", [1, 2, :L] => "1,2 L", [2, 1, :L] => "2,1 L", [2, 2, :L] => "2,2 L", [0, 0, :R] => "0,0 R", [1, 0, :R] => "1,0 R", [0, 1, :R] => "0,1 R", [1, 1, :R] => "1,1 R", [0, 2, :R] => "0,2 R", [2, 0, :R] => "2,0 R", [1, 2, :R] => "1,2 R", [2, 1, :R] => "2,1 R", [2, 2, :R] => "2,2 R", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def triangle_grid_vertex_coordinates grid = Grid.new(Triangle.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1, :L] => "red", [1, 1, :R] => "red", [2, 1, :L] => "gray", [2, 1, :R] => "gray", [1, 2, :L] => "gray", [1, 2, :R] => "gray", [2, 2, :L] => "gray", [2, 2, :R] => "gray", } grid.highlight_vertices = { [1, 1] => "red", [2, 1] => "black", [1, 2] => "black", [2, 2] => "black", } grid.annotate_vertices = { [1, 1] => "1,1", [2, 1] => "2,1", [1, 2] => "1,2", [2, 2] => "2,2", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end def triangle_grid_edge_coordinates grid = Grid.new(Triangle.new, 70) grid.add_facearray(0...3, 0...3) grid.highlight_faces = { [1, 1, :L] => "red", [1, 1, :R] => "red", [1, 2, :L] => "gray", [2, 1, :L] => "gray", } grid.highlight_edges = { [1, 1, :S] => "red", [1, 1, :W] => "red", [1, 1, :E] => "red", [1, 2, :S] => "black", [2, 1, :W] => "black", } grid.annotate_edges = { [1, 1, :S] => "1,1 S", [1, 1, :W] => "1,1 W", [1, 1, :E] => "1,1 E", [1, 2, :S] => "1,2 S", [2, 1, :W] => "2,1 W", } canvas = Canvas.new(400, 300, grid) canvas.to_svg end #################### def grid_parts grid = Grid.new(Triangle.new, 86) grid.add_faces([0, 0, :R], [2, 1, :L]) grid.add_facearray(1...3, 0...1) grid.add_facearray(0...2, 1...2) grid.highlight_faces = { [1, 0, :L] => "red" } grid.highlight_edges = { [0, 1, :E] => "red" } grid.highlight_vertices = { [2, 1] => "red" } grid.annotate_faces = { [1, 0, :L] => "face" } grid.annotate_edges = { [0, 1, :E] => "edge" } grid.annotate_vertices = { [2, 1] => "vertex" } canvas = Canvas.new(400, 300, grid) canvas.to_svg end end diagrams = Diagrams.new File.new('square-grid.svg', 'w') << diagrams.square_grid File.new('hexagon-grid.svg', 'w') << diagrams.hexagon_grid File.new('triangle-grid.svg', 'w') << diagrams.triangle_grid File.new('grid-parts.svg', 'w') << diagrams.grid_parts File.new('square-to-hexagon-2.svg', 'w') << diagrams.hexagon_grid(9, 0.0) File.new('square-to-hexagon-3.svg', 'w') << diagrams.hexagon_grid(9, 0.5) File.new('square-grid-small.svg', 'w') << diagrams.square_grid(5) File.new('rhombus-grid-small.svg', 'w') << diagrams.rhombus_grid(5) File.new('triangle-grid-small.svg', 'w') << diagrams.triangle_grid_small File.new('square-grid-face-coordinates.svg', 'w') << diagrams.square_grid_face_coordinates File.new('square-grid-vertex-coordinates.svg', 'w') << diagrams.square_grid_vertex_coordinates File.new('square-grid-edge-coordinates.svg', 'w') << diagrams.square_grid_edge_coordinates File.new('hexagon-grid-face-coordinates.svg', 'w') << diagrams.hexagon_grid_face_coordinates File.new('hexagon-grid-vertex-coordinates.svg', 'w') << diagrams.hexagon_grid_vertex_coordinates File.new('hexagon-grid-edge-coordinates.svg', 'w') << diagrams.hexagon_grid_edge_coordinates File.new('triangle-grid-face-coordinates.svg', 'w') << diagrams.triangle_grid_face_coordinates File.new('triangle-grid-vertex-coordinates.svg', 'w') << diagrams.triangle_grid_vertex_coordinates File.new('triangle-grid-edge-coordinates.svg', 'w') << diagrams.triangle_grid_edge_coordinates File.new('square-rel-face-face.svg', 'w') << diagrams.square_rel_face_face File.new('square-rel-face-edge.svg', 'w') << diagrams.square_rel_face_edge File.new('square-rel-face-vertex.svg', 'w') << diagrams.square_rel_face_vertex File.new('square-rel-edge-face.svg', 'w') << diagrams.square_rel_edge_face File.new('square-rel-edge-edge.svg', 'w') << diagrams.square_rel_edge_edge File.new('square-rel-edge-vertex.svg', 'w') << diagrams.square_rel_edge_vertex File.new('square-rel-vertex-face.svg', 'w') << diagrams.square_rel_vertex_face File.new('square-rel-vertex-edge.svg', 'w') << diagrams.square_rel_vertex_edge File.new('square-rel-vertex-vertex.svg', 'w') << diagrams.square_rel_vertex_vertex File.new('hex-grid-metrics.svg', 'w') << diagrams.hex_grid_metrics