Clone in a terminal:
PenTool.vala
/*
    Copyright (C) 2012 Johan Mattsson

    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
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
*/

namespace Supplement {

class PenTool : Tool {

	private static const double CONTACT_SURFACE = 50;

	public static bool move_selected = false;
	public static bool begin_new_point_on_path = false;
	public static bool move_point_on_path = false;

	public static bool edit_active_corner = false;
	
	public static List<EditPoint> selected_points = new List<EditPoint> (); 

	public static EditPointHandle active_handle = new EditPointHandle.empty ();
	public static EditPointHandle selected_handle = new EditPointHandle.empty ();
	
	public static EditPoint? active_edit_point = new EditPoint ();

	/** Move handle vertical or horizontal. */
	public static bool tie_x_or_y_coordinates = false;

	private static bool move_selected_handle = false;

	private static double last_point_x = 0;
	private static double last_point_y = 0;

	public static double precision = 1;
	
	public PenTool (string name) {
		base (name, _("Right click to add new points, left click to move points") + " " + _("and double click to add new point on path."), ',', CTRL);
		
		select_action.connect ((self) => {
		});

		deselect_action.connect ((self) => {
			Glyph glyph = MainWindow.get_current_glyph ();
			
			CutTool.force_direction ();
			glyph.close_path ();
			
			move_point_on_path = false;
			begin_new_point_on_path = false;
		});
		
		press_action.connect ((self, b, x, y) => {
			last_point_x = x;
			last_point_y = y;

			press (b, x, y, false);
		});
		
		double_click_action.connect ((self, b, x, y) => {
			last_point_x = x;
			last_point_y = y;

			press (b, x, y, true);
		});

		release_action.connect ((self, b, ix, iy) => {
			double x = ix;
			double y = iy;
			Glyph g = MainWindow.get_current_glyph ();
			EditPoint selected_corner;
			
			if (move_selected && GridTool.is_visible () && selected_points.length () > 0) {
				selected_corner = selected_points.last ().data;
				GridTool.tie (ref x, ref y);
				g.move_selected_edit_point (selected_corner, x, y);
			}

			move_selected = false;
			move_selected_handle = false;
			edit_active_corner = false;
			
			active_handle = new EditPointHandle.empty ();
			
			move (x, y);
		});

		move_action.connect ((self, x, y) => {
			move (x, y);
		});
		
		key_press_action.connect ((self, keyval) => {
			Glyph g = MainWindow.get_current_glyph ();
			
			if (keyval == Key.DEL) {
				foreach (EditPoint p in selected_points) {
					g.delete_edit_point (p);
				}
				
				g.update_view ();
			}
		});
		
		key_release_action.connect ((self, keyval) => {
		});
	}
	
	public double get_precision () {
		return precision;
	}
	
	public void set_precision (double p) {
		precision = p;
		MainWindow.get_toolbox ().precision.set_value_round (p, false, false);
	}
	
	public void move (double x, double y) {
		Glyph glyph = MainWindow.get_current_glyph ();
		
		control_point_event (x, y);
		curve_active_corner_event (x, y);
		set_default_handle_positions ();

		// show new point on path
		if (glyph.new_point_on_path != null) {
			move_current_point_on_path (x, y);
		}
		
		// move curve handles
		if (move_selected_handle) {
			if (GridTool.is_visible ()) {
				GridTool.tie (ref x, ref y);
				selected_handle.set_point_type (PointType.CURVE);
				selected_handle.move_to (x, y);
			} else {
				selected_handle.set_point_type (PointType.CURVE);
				selected_handle.move_delta ((x - last_point_x) * precision, (y - last_point_y) * precision);
			}
			
			selected_handle.parent.recalculate_linear_handles ();
			
			// Fixa: redraw line only
			glyph.redraw_area (0, 0, glyph.allocation.width, glyph.allocation.height);
						
			last_point_x = x;
			last_point_y = y;
			
			return;
		}
		
		// move edit point
		if (move_selected) {
			foreach (EditPoint p in selected_points) {
				glyph.move_selected_edit_point_delta (p, (x - last_point_x) * precision, (y - last_point_y) * precision);
				
				if (tie_x_or_y_coordinates) {
					GridTool.tie_to_prev (p, x, y);
				}
				
				p.recalculate_linear_handles ();
			}
		}
		
		last_point_x = x;
		last_point_y = y;
	}
	
	public void press (int button, int x, int y, bool double_click) {
		Glyph? g = MainWindow.get_current_glyph ();
		Glyph glyph = (!) g;
		
		return_if_fail (g != null);

		if (button == 2) {
			if (glyph.is_open ()) {
				CutTool.force_direction ();
				glyph.close_path ();
			} else {
				glyph.open_path ();
			}
			
			return;
		}

		if (double_click) {
			glyph.insert_new_point_on_path (x, y);
			return;
		}

		if (is_new_point_from_path_selected ()) {
			new_point_on_path_at (x, y);
			return;	
		}

		// add new point on path 
		if (is_new_point_from_path_selected ()) {
			move_selected = true;
			move_point_on_path = true;
			glyph.new_point_on_path = null;
			return;
		}
		
		// add new point
		if (button == 3) {
			remove_all_selected_points ();
			new_point_action (x, y);
			glyph.store_undo_state ();
			return;
		}
				
		control_point_event (x, y);
		curve_corner_event (x, y);
		
		if (!move_selected_handle) {
			select_active_point (x, y);
		}

		glyph.store_undo_state ();
	}
	
	public void select_active_point (double x, double y) {
		Glyph? g = MainWindow.get_current_glyph ();
		Glyph glyph = (!) g;
		
		if (KeyBindings.modifier != SHIFT) {
			if (active_edit_point != null) {
				((!)active_edit_point).set_active (false);
			}

			remove_all_selected_points ();
		}
		
		move_selected = true;
		move_point_on_path = true;

		if (active_edit_point != null) {
			((!)active_edit_point).set_selected (true);
		}

		if (!is_over_handle (x, y)) {
			edit_active_corner = true;
			set_default_handle_positions ();
			
			if (active_edit_point != null) {
				add_selected_point ((!) active_edit_point);
			}
		}
		
		// continue adding points from the selected one
		foreach (Path p in glyph.active_paths) {
			if (p.is_open () && p.points.length () > 0 && active_edit_point == p.points.first ().data) {
				p.reverse ();
			}
		}
	}
	
	public void move_current_point_on_path (double x, double y) {
		Glyph g = MainWindow.get_current_glyph ();

		EditPoint e;
		
		double rax, ray;
		double pax, pay;

		double distance, min;
		
		return_if_fail (g.new_point_on_path != null);
		
		if (GridTool.is_visible ()) {
			GridTool.tie (ref x, ref y);
		}

		min = double.MAX;

		foreach (Path p in g.path_list) {
			if (p.points.length () < 2) {
				continue;
			}
			
			e = new EditPoint ();
			
			pax = x * Glyph.ivz () + g.view_offset_x - Glyph.xc ();
			pay = y * Glyph.ivz () + g.view_offset_y - Glyph.yc ();

			pay *= -1;

			p.get_closest_point_on_path (e, pax, pay);
			
			distance = Math.sqrt (Math.pow (Math.fabs (pax - e.x), 2) + Math.pow (Math.fabs (pay - e.y), 2));
			
			if (distance < min) {
				min = distance;
				
				g.new_point_on_path = e;
				
				rax = (pax + g.view_offset_x - Glyph.xc ()) / Glyph.ivz ();
				ray = (pax + g.view_offset_x - Glyph.xc ()) / Glyph.ivz ();
				
				g.redraw_area (rax - 5, ray - 5, 10, 10);					
			}
		}
	}

	public void set_new_point_on_path (Path ap, int x, int y) {
		Glyph g = MainWindow.get_current_glyph ();
		
		return_if_fail (ap.is_editable ());
		
		g.clear_active_paths ();
		g.add_active_path (ap);

		if (g.new_point_on_path == null) {
			g.new_point_on_path = new EditPoint (0, 0, PointType.FLOATING);
		}
		
		move_current_point_on_path (x, y);
	}
		
	public static void set_active_edit_point (EditPoint? e) {
		Glyph g = MainWindow.get_current_glyph ();
		foreach (var p in g.path_list) {
			foreach (var ep in p.points) {
				ep.set_active (false);
			}
		}
		
		active_edit_point = e;
		
		if (e != null) {
			((!)e).set_active (true);
		}

		g.redraw_area (0, 0, g.allocation.width, g.allocation.height);
	}

	EditPoint? get_closest_point (double ex, double ey, out Path? path) {
		double x = Glyph.path_coordinate_x (ex);
		double y = Glyph.path_coordinate_y (ey);
		double d = double.MAX;
		double nd;
		EditPoint? ep = null;
		Glyph g = MainWindow.get_current_glyph ();
		
		path = null;
		
		foreach (Path current_path in g.path_list) {
			foreach (EditPoint e in current_path.points) {
				nd = e.get_distance (x, y);
				
				if (nd < d) {
					d = nd;
					ep = e;
					path = current_path;
				}
			}	
		}
		
		return ep;
	}

	public double get_distance_to_closest_edit_point (double event_x, double event_y) {
		Path? p;
		EditPoint e;
		EditPoint? ep = get_closest_point (event_x, event_y, out p);

		double x = Glyph.path_coordinate_x (event_x);
		double y = Glyph.path_coordinate_y (event_y);
		
		if (ep == null) {
			return double.MAX;
		}
		
		e = (!) ep;
		
		return e.get_distance (x, y);
	}

	public void control_point_event (double event_x, double event_y) {
		Path? p;
		EditPoint? ep = get_closest_point (event_x, event_y, out p);
		Glyph g = MainWindow.get_current_glyph ();
		double x = Glyph.path_coordinate_x (event_x);
		double y = Glyph.path_coordinate_y (event_y);
		double distance;
		EditPoint e;
		
		set_active_edit_point (null);
		
		if (ep == null) {
			return;	
		}
		
		e = (!) ep;
		distance = e.get_distance (x, y) * g.view_zoom;

		if (distance < CONTACT_SURFACE) {
			set_active_edit_point (ep);
			g.add_active_path (p);
		}
	}

	public static bool insert_new_point_on_path_selected () {
		Tool t = MainWindow.get_toolbox ().get_tool ("insert_point_on_path");
		return t.is_selected ();
	}

	public static bool is_new_point_from_path_selected () {
		if (!Supplement.experimental) {
			return false;
		}
		
		Tool t = MainWindow.get_toolbox ().get_tool ("new_point_on_path");
		return t.is_selected ();
	}

	public static bool is_erase_selected () {
		Tool t = MainWindow.get_toolbox ().get_tool ("erase_tool");
		return t.is_selected ();
	}
	
	public void new_point_action (int x, int y) {
		Glyph glyph;
		EditPoint new_point;
		glyph = MainWindow.get_current_glyph ();
		glyph.open_path ();
		
		new_point = glyph.add_new_edit_point (x, y);
		new_point.set_selected (true);
		
		if (KeyBindings.modifier != SHIFT) {
			remove_all_selected_points ();
		}
		
		add_selected_point (new_point);
		
		move_selected = true;
	}

	void set_linear_handles_for_last_point () {
		Glyph glyph;
		glyph = MainWindow.get_current_glyph ();
		return_if_fail (glyph.active_paths.length () > 0);
		return_if_fail (glyph.active_paths.last ().data.points.length () > 0);
		glyph.active_paths.last ().data.points.last ().data.right_handle.parent.recalculate_linear_handles ();
	}

	public static void set_default_handle_positions () {
		Glyph g = MainWindow.get_current_glyph ();
		foreach (var p in g.path_list) {
			if (p.is_editable ()) {
				p.create_list ();
				set_default_handle_positions_on_path (p);
			}
		}
	}

	static void set_default_handle_positions_on_path (Path path) {
		foreach (EditPoint e in path.points) {
			e.recalculate_linear_handles ();
		}
	}

	bool new_point_on_path_at (int x, int y) {
		EditPoint ep;
		Glyph glyph = MainWindow.get_current_glyph ();
		
		if (is_new_point_from_path_selected ()) {
			
			if (glyph.new_point_on_path != null) {
				ep = (!)glyph.new_point_on_path; 
				
				begin_new_point_on_path = false;
				
				glyph.add_new_edit_point (glyph.reverse_path_coordinate_x (ep.x), glyph.reverse_path_coordinate_x (ep.y));
				glyph.new_point_on_path = null;
				
				MainWindow.get_toolbox ().select_tool_by_name ("new_point");

				return false;
			}
			
			start_from_new_point_on_path (x, y);
			begin_new_point_on_path = true;
			
			return false;
		} else {
			begin_new_point_on_path = false;
		}
		
		return true;
	}

	void start_from_new_point_on_path (double x, double y) {		
		Glyph g = MainWindow.get_current_glyph ();
		Path? p = null;
		Path pn;
		
		p = g.get_closeset_path (x, y);

		pn = (!)p;

		if (pn.points.length () >= 2) {
			pn.set_editable (true);
			set_new_point_on_path (pn, 0, 0);
			g.open_path ();
		}
		
		begin_new_point_on_path = false;
	}
	
	public void begin_from_new_point_on_path () {
		begin_new_point_on_path = true;
	}
	
	private bool is_over_handle (double event_x, double event_y) {
		double x = Glyph.path_coordinate_x (event_x);
		double y = Glyph.path_coordinate_y (event_y);
		Glyph g = MainWindow.get_current_glyph (); 
		double d_point = g.view_zoom * get_distance_to_closest_edit_point (event_x, event_y);
		
		double dl, dr;
	
		foreach (EditPoint selected_corner in selected_points) {
			dl = g.view_zoom * selected_corner.get_left_handle ().get_point ().get_distance (x, y);
			dr = g.view_zoom * selected_corner.get_right_handle ().get_point ().get_distance (x, y);
			
			if (dl < CONTACT_SURFACE && dl < d_point) {
				return true;
			}

			if (dr < CONTACT_SURFACE && dr < d_point) {
				return true;
			}
		}
	
		return false;
	}

	EditPointHandle get_closest_handle (double event_x, double event_y) {
		EditPointHandle left, right;
		double x = Glyph.path_coordinate_x (event_x);
		double y = Glyph.path_coordinate_y (event_y);		
		EditPointHandle eh = new EditPointHandle.empty();

		double d = double.MAX;
		double dn;
				
		foreach (EditPoint selected_corner in selected_points) {
			left = selected_corner.get_left_handle ();
			right = selected_corner.get_right_handle ();

			dn = left.get_point ().get_distance (x, y);
			
			if (dn < d) {
				eh = left;
				d = dn;
			}

			dn = right.get_point ().get_distance (x, y);
			
			if (dn < d) {
				eh = right;
				d = dn;
			}
		}
		
		return eh;
	}

	private void curve_active_corner_event (double event_x, double event_y) {
		EditPointHandle eh;
		
		active_handle.active = false;
		
		if (!is_over_handle (event_x, event_y)) {
			return;
		}		
		
		eh = get_closest_handle (event_x, event_y);
		eh.active = true;
		active_handle = eh;
	}

	private void curve_corner_event (double event_x, double event_y) {
		MainWindow.get_current_glyph ().open_path ();

		if (!is_over_handle (event_x, event_y)) {
			return;
		}

		move_selected_handle = true;
		selected_handle = get_closest_handle (event_x, event_y);
	}

	void add_selected_point (EditPoint p) {
		foreach (EditPoint ep in selected_points) {
			if (p == ep) {
				return;
			}
		}
		
		selected_points.append (p);
	}
	
	void remove_all_selected_points () {
		foreach (EditPoint e in selected_points) {
			e.set_active (false);
			e.set_selected (false);
		}
			
		while (selected_points.length () > 0) {
			selected_points.remove_link (selected_points.first ());
		}
	}
	
	/** Draw a test glyph. */
	public override bool test () {
		test_select_action ();
		
		test_open_next_glyph ();
		
		// paint
		test_click_action (1, 30, 30); 
		test_click_action (1, 60, 30);
		test_click_action (1, 60, 60);
		test_click_action (1, 30, 60);
		
		// close
		test_click_action (3, 0, 0);

		// reopen
		test_click_action (3, 35, 35);
		
		// move around
		test_move_action (100, 200);
		test_move_action (20, 300);
		test_move_action (0, 0);
		
		// add to path
		test_move_action (70, 50);
		
		test_click_action (1, 70, 50);
		test_click_action (1, 70, 50);
		test_click_action (1, 70, 100);
		test_click_action (1, 50, 100); 
		test_click_action (1, 50, 50);
		
		// close
		test_click_action (3, 0, 0);
		
		// merge it
		
		
		this.yield ();
		
		return true;
	}

	public void test_active_edit_point () {
		Glyph g;
		EditPoint epa, epb;
		
		// paint
		test_select_action ();
		test_open_next_glyph ();

		g = MainWindow.get_current_glyph ();

		test_click_action (1, 130, 130); // open path
		test_click_action (1, 130, 130); // add point
		epa = g.get_last_edit_point ();
		
		test_click_action (1, 160, 130);
		test_click_action (1, 160, 160);
		epb = g.get_last_edit_point ();
		
		test_click_action (1, 130, 160);
		
		// validate active point
		test_move_action (130, 130);
		warn_if_fail (active_edit_point == epa);
		
		test_move_action (161, 161); // close but not on
		warn_if_fail (active_edit_point == epb);
		
		warn_if_fail (epa != epb);
		
		// TODO: Test move handle here.
	}

	/** Test path coordinates and reverse path coordinates. */
	public void test_coordinates () {
		int x, y, xc, yc;
		double px, py, mx, my;
		string n;
		
		Tool.test_open_next_glyph ();
		Glyph g = MainWindow.get_current_glyph ();
		
		xc = (int) (g.allocation.width / 2.0);
		yc = (int) (g.allocation.height / 2.0);

		g.default_zoom ();
		
		x = 10;
		y = 15;
		
		px = g.path_coordinate_x (x);
		py = g.path_coordinate_y (y);

		mx = x * g.ivz () - g.xc () + g.view_offset_x;
		my = g.yc () - y * g.ivz () - g.view_offset_y;
		
		if (mx != px || my != py) {
			warning (@"bad coordinate $mx != $px || $my != $py");
		}
			
		test_reverse_coordinate (x, y, px, py, "ten fifteen");
		test_click_action (1, x, y);
	
		// offset no zoom
		n = "Offset no zoom";
		g.reset_zoom ();
		
		px = g.path_coordinate_x (x);
		py = g.path_coordinate_y (y);
		
		test_reverse_coordinate (x, y, px, py, n);
		test_click_action (1, x, y);
		
		// close path
		test_click_action (3, x, y);
	}
	
	private void test_coordinate (double x, double y, double px, double py, string n) {
		if (!(px - 0.5 <= x <= px + 0.5|| py - 0.5 <= y <= py + 0.5)) {
			warning (@"Expecting (x == px || y == py) got ($x == $px || $y == $py) in \"$n\"\n");
		}
	}
	
	private void test_reverse_coordinate (int x, int y, double px, double py, string n) {
		Glyph g = MainWindow.get_current_glyph ();
		if (x != g.reverse_path_coordinate_x (px) || g.reverse_path_coordinate_y (py) != y) {
			warning (@"Reverse coordinates does not match current point for test case \"$n\".\n $x != $(g.reverse_path_coordinate_x (px)) || $(g.reverse_path_coordinate_y (py)) != $y (x != g.reverse_path_coordinate_x (px) || g.reverse_path_coordinate_y (py) != y)");
		}
	}

	private void test_last_is_counter_clockwise (string name) {
		bool d = ((!)MainWindow.get_current_glyph ().get_last_path ()).is_clockwise ();
		
		if (d) {
			critical (@"\nPath $name is clockwise, in test_last_is_counter_clockwise");
		}		
	}
	
	private void test_last_is_clockwise (string name) {
		bool d = ((!)MainWindow.get_current_glyph ().get_last_path ()).is_clockwise ();
		
		if (!d) {
				critical (@"\nPath $name is counter clockwise, in test_last_is_clockwise");
		}

	}
	
	private bool test_reverse_last (string name) 
		requires (MainWindow.get_current_glyph ().get_last_path () != null)
	{
		Glyph g = MainWindow.get_current_glyph ();
		Path p = (!) g.get_last_path ();
		bool direction = p.is_clockwise ();

		p.reverse ();
		
		if (direction == p.is_clockwise ()) {
			critical (@"Direction did not change after reverseing path \"$name\"\n");
			stderr.printf (@"Path length: $(p.points.length ()) \n");
			return false;
		}

		this.yield ();
		return true;
	}
	
	
	class Point {
		
		public int x;
		public int y;
		
		public Point (int x, int y) {
			this.x = x;
			this.y = y;
		}
	}
	
	private Point p (int x, int y) {
		return new Point (x, y);
	}
	
	private void test_triangle (Point a, Point b, Point c, string name = "") {
		Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");
		
		this.yield ();
		MainWindow.get_tab_bar ().select_overview ();

		this.yield ();
		MainWindow.get_overview ().open_current_glyph ();
		
		pen_tool.test_select_action ();
		
		pen_tool.test_click_action (1, 0, 0); // open path
		
		pen_tool.test_click_action (1, a.x, a.y);
		pen_tool.test_click_action (1, b.x, b.y);
		pen_tool.test_click_action (1, c.x, c.y);
		pen_tool.test_click_action (3, 0, 0);
		
		test_reverse_last (@"Triangle reverse \"$name\" ($(a.x), $(a.y)), ($(b.x), $(b.y)), ($(c.x), $(c.y)) failed.");
		
		this.yield ();
	}
	
	private void test_various_triangles () {
		test_triangle (p (287, 261), p (155, 81), p (200, 104), "First");
		test_triangle (p (65, 100), p (168, 100), p (196, 177), "Second");
		test_triangle (p (132, 68), p (195, 283), p (195, 222), "Third");
		test_triangle (p (144, 267), p (147, 27), p (296, 267), "Fourth");
	}
	
	public bool test_reverse_path () {
		// open a new glyph
		Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");

		this.yield ();
		MainWindow.get_tab_bar ().select_overview ();

		this.yield ();
		MainWindow.get_overview ().open_current_glyph ();
		
		pen_tool.test_select_action ();
		
		// paint
		int x_offset = 10;
		int y_offset = 10;
		
		test_open_next_glyph ();
		test_various_triangles ();
		
		test_open_next_glyph ();
		// draw clockwise and check direction		

		y_offset += 160;
		pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (1, 17 + x_offset, 17 + y_offset);
		pen_tool.test_click_action (1, 20 + x_offset, 0 + y_offset);
		pen_tool.test_click_action (3, 0, 0);
		test_last_is_clockwise ("Clockwise triangle 1.2");

		// draw paths clockwise / counter clockwise and reverse them
		
		pen_tool.test_click_action (1, 115, 137);
		pen_tool.test_click_action (1, 89, 74);
		pen_tool.test_click_action (1, 188, 232);
		pen_tool.test_click_action (3, 0, 0);
		test_reverse_last ("Triangle 0");

		// draw incomplete paths
		y_offset += 20;
		pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (3, 0, 0);
		test_reverse_last ("Point");

		y_offset += 20;
		pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (3, 0, 0);
		test_reverse_last ("Double point");

		y_offset += 20;
		pen_tool.test_click_action (1, 10 + x_offset, 30 + y_offset);
		pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (3, 0, 0);
		test_reverse_last ("Vertical line");

		y_offset += 20;
		pen_tool.test_click_action (1, 30 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (3, 0, 0);
		test_reverse_last ("Horisontal line");

		// triangle 1
		y_offset += 20;
		pen_tool.test_click_action (1, 10 + x_offset, -10 + y_offset);
		pen_tool.test_click_action (1, 20 + x_offset, 20 + y_offset);
		pen_tool.test_click_action (1, 30 + x_offset, 0 + y_offset);
		pen_tool.test_click_action (3, 0, 0);
		test_reverse_last ("Triangle reverse 1");

		// box
		y_offset += 20;
		pen_tool.test_click_action (1, 100 + x_offset, 150 + y_offset);
		pen_tool.test_click_action (1, 150 + x_offset, 150 + y_offset);
		pen_tool.test_click_action (1, 150 + x_offset, 100 + y_offset);
		pen_tool.test_click_action (1, 100 + x_offset, 100 + y_offset); 
		pen_tool.test_click_action (3, 0, 0); // close
		test_reverse_last ("Box 1");
		
		return true;
	}
	
	private Tool select_pen () {
		Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");
		pen_tool.test_select_action ();
		return pen_tool;
	}
	
	public bool test_delete_points () {
		PenTool pen;
		Tool delete_tool = MainWindow.get_toolbox ().get_tool ("erase_tool");
		
		test_open_next_glyph ();
				
		pen = (PenTool) select_pen ();
		pen.test_click_action (1, 0, 0); // open path
		
		// draw a line with ten points
		for (int i = 1; i <= 10; i++) {
			pen.test_click_action (1, 20*i, 20);
		}	
	
		// TODO: it would be nice to test if points were created here
		
		// delete points
		delete_tool.test_select_action ();
		for (int i = 1; i <= 10; i++) {
			pen.move (20*i, 20);
			pen.test_click_action (1, 20*i, 20);
		}
		
		return true;
	}
	
	public bool test_reverse_random_triangles () {
		Tool pen;
		
		int ax, bx, cx;
		int ay, by, cy;

		bool r = true;

		test_open_next_glyph ();
		pen = select_pen ();

		for (int i = 0; i < 30; i++) {
			this.yield ();
			
			ax = Random.int_range (0, 300);
			bx = Random.int_range (0, 300);
			cx = Random.int_range (0, 300);

			ay = Random.int_range (0, 300);
			by = Random.int_range (0, 300);
			cy = Random.int_range (0, 300);

			pen.test_click_action (1, ax, ay);
			pen.test_click_action (1, bx, by);
			pen.test_click_action (1, cx, cy);
			pen.test_click_action (3, 0, 0);
		
			r = test_reverse_last (@"Random triangle № $(i + 1) ($ax, $ay), ($bx, $by), ($cx, $cy)");
			if (!r) {
				test_open_next_glyph ();
				pen = select_pen ();

				pen.test_click_action (1, ax, ay);
				pen.test_click_action (1, bx, by);
				pen.test_click_action (1, cx, cy);
		
				return false;
			}
			
			test_open_next_glyph ();
		}
		
		if (r) test_open_next_glyph ();
		
		return true;
	}
}

}

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112