#!/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
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