--- /dev/null
+#!/usr/bin/env python
+
+# This file is part of LilyPond, the GNU music typesetter.
+#
+# Copyright (C) 2012 Joe Neeman <joeneeman@gmail.com>
+#
+# LilyPond 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.
+#
+# LilyPond 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 LilyPond. If not, see <http://www.gnu.org/licenses/>.
+
+# A GTK+ program for debugging skylines. The program reads a sequence
+# of line segments from stdin (one line segment per line of stdin, in the format
+# '(x1, y1) (x2, y2)'). A skyline is terminated by an empty line, which
+# causes the skyline to be displayed on the screen.
+
+from threading import Thread
+from math import isinf
+import gtk
+import gobject
+import goocanvas
+import sys
+import re
+
+class GtkSkylineCanvas (goocanvas.Canvas):
+ """A Canvas for displaying skylines."""
+ def __init__ (self):
+ super (GtkSkylineCanvas, self).__init__ ()
+ self.connect ('size-allocate', GtkSkylineCanvas.rescale)
+ self.x_min = float ('inf')
+ self.x_max = float ('-inf')
+ self.y_min = float ('inf')
+ self.y_max = float ('-inf')
+
+ self.colors = ('black', 'red', 'green', 'blue', 'maroon', 'olive', 'teal')
+ self.cur_color_index = 0
+
+ def rescale (self, allocation):
+ width = (self.x_max - self.x_min + 1) * 1.1
+ height = (self.y_max - self.y_min + 1) * 1.1
+ if width <= 0 or height <= 0:
+ return
+
+ scale_x = allocation.width / width
+ scale_y = allocation.height / height
+ scale = min (scale_x, scale_y)
+ self.set_scale (scale)
+
+ center_x = (self.x_max + self.x_min) / 2
+ center_y = (self.y_max + self.y_min) / 2
+ actual_width = allocation.width / scale
+ actual_height = allocation.height / scale
+ actual_min_x = center_x - actual_width / 2
+ actual_max_x = center_x + actual_width / 2
+ actual_min_y = center_y - actual_height / 2
+ actual_max_y = center_y + actual_height / 2
+
+ self.set_bounds (actual_min_x, actual_min_y, actual_max_x, actual_max_y)
+ self.scroll_to (actual_min_x, actual_min_y)
+
+ def add_skyline (self, lines):
+ """Adds a skyline to the current canvas, in a new color.
+
+ The canvas will be rescaled, if necessary, to make room for the
+ new skyline."""
+ # Flip vertically, because goocanvas thinks higher numbers are
+ # further down, while lilypond thinks they're further up.
+ lines = [(x1, -y1, x2, -y2) for (x1, y1, x2, y2) in lines]
+
+ color = self.colors[self.cur_color_index]
+ self.cur_color_index = (self.cur_color_index + 1) % len (self.colors)
+
+ # Update the bounding box of the skylines.
+ x_vals = [s[0] for s in lines] + [s[2] for s in lines]
+ y_vals = [s[1] for s in lines] + [s[3] for s in lines]
+ self.x_min = min ([self.x_min] + x_vals)
+ self.x_max = max ([self.x_max] + x_vals)
+ self.y_min = min ([self.y_min] + y_vals)
+ self.y_max = max ([self.y_max] + y_vals)
+
+ # Add the lines to the canvas.
+ root = self.get_root_item ()
+ for (x1, y1, x2, y2) in lines:
+ goocanvas.polyline_new_line (root, x1, y1, x2, y2,
+ stroke_color=color,
+ line_width=0.05)
+ self.rescale (self.get_allocation ())
+
+# We want to run the gtk main loop in a separate thread so that
+# the main thread can be responsible for reading stdin.
+class SkylineWindowThread (Thread):
+ """A thread that runs a Gtk.Window displaying a skyline."""
+
+ def run (self):
+ gtk.gdk.threads_init ()
+ self.window = None
+ self.canvas = None
+ gtk.main ()
+
+ # This should only be called from the Gtk main loop.
+ def _destroy_window (self, window):
+ sys.exit (0)
+
+ # This should only be called from the Gtk main loop.
+ def _setup_window (self):
+ if self.window is None:
+ self.window = gtk.Window ()
+ self.canvas = GtkSkylineCanvas ()
+ self.window.add (self.canvas)
+ self.window.connect ("destroy", self._destroy_window)
+ self.window.show_all ()
+
+ # This should only be called from the Gtk main loop.
+ def _add_skyline (self, lines):
+ self._setup_window ()
+ self.canvas.add_skyline (lines)
+
+ def add_skyline (self, lines):
+ # Copy the lines, just in case someone modifies them.
+ gobject.idle_add (self._add_skyline, list (lines))
+
+thread = SkylineWindowThread ()
+thread.setDaemon (True)
+thread.start ()
+
+def lines(infile):
+ line = infile.readline()
+ while len(line) > 0:
+ yield line[:-1]
+ line = infile.readline()
+
+point_re_str = r'\(([a-z.0-9-]*) *,([a-z0-9.-]*)\)'
+line_re_str = point_re_str + r' +' + point_re_str
+line_re = re.compile (line_re_str)
+
+# The main loop just reads lines from stdin and feeds them to the
+# display.
+current_skyline = []
+for line in lines(sys.stdin):
+ if not line:
+ thread.add_skyline(current_skyline)
+ current_skyline = []
+ continue
+
+ m = re.search (line_re, line)
+ if m is None:
+ print('line did not match')
+ else:
+ pts = map(float, m.groups())
+ if not any(map(isinf, pts)):
+ current_skyline.append(pts)