Clone in a terminal:
BezierTool.vala
/*
	Copyright (C) 2015 Johan Mattsson

	This library is free software; you can redistribute it and/or modify 
	it under the terms of the GNU Lesser General Public License as 
	published by the Free Software Foundation; either version 3 of the 
	License, or (at your option) any later version.

	This library 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 
	Lesser General Public License for more details.
*/

using Math;
using Cairo;

namespace BirdFont {

/** Create Bezier curves. */
public class BezierTool : Tool {
	
	public const uint NONE = 0;
	public const uint MOVE_POINT = 1;
	public const uint MOVE_HANDLES = 2;
	public const uint MOVE_LAST_HANDLE_RIGHT = 3;
	public const uint MOVE_LAST_HANDLE_LEFT = 4;
	public const uint MOVE_FIRST_HANDLE = 5;
	public const uint MOVE_HANDLE_ON_AXIS = 6;
	
	uint state = NONE;
	
	Path current_path = new Path ();
	EditPoint current_point = new EditPoint ();

	int last_x = 0;
	int last_y = 0;

	double last_release_time = 0;
	double last_press_time = 0;
	bool button_down = false;
	
	/** Create a corner instead of a control point with reflective handles. */
	bool corner_node = false;
	
	/** Swap right and left handles if orientation changes */
	bool swap = false; 
	
	public BezierTool (string name) {
		base (name, "");

		select_action.connect ((self) => {
			state = NONE;
			MainWindow.set_cursor (NativeWindow.VISIBLE);
		});

		deselect_action.connect ((self) => {
			state = NONE;
			MainWindow.set_cursor (NativeWindow.VISIBLE);
		});
		
		press_action.connect ((self, b, x, y) => {
			press (b, x, y);
		});

		double_click_action.connect ((self, b, x, y) => {
		});

		release_action.connect ((self, b, x, y) => {
			release (b, x, y);
		});

		move_action.connect ((self, x, y) => {
			move (x, y);
		});
		
		key_press_action.connect ((self, keyval) => {
		});
		
		key_release_action.connect ((self, keyval) => {
		});
		
		draw_action.connect ((tool, cairo_context, glyph) => {
			if (PenTool.can_join (current_point)) {
				PenTool.draw_join_icon (cairo_context, last_x, last_y);
			}
		});
	}
	
	public override string get_tip () {
		string tip = t_ ("Create Bézier curves") + "\n";
		
		tip += HiddenTools.bezier_line.get_key_binding ();
		tip += " - ";
		tip += t_ ("line") + "\n";

		tip += HiddenTools.bezier_corner.get_key_binding ();
		tip += " - ";
		tip += t_ ("corner") + "\n";

		tip += HiddenTools.move_along_axis.get_key_binding ();
		tip += " - ";
		tip += t_ ("on axis") + "\n";
				
		return tip;
	}
	
	public void press (int b, int x, int y) {
		Glyph g = MainWindow.get_current_glyph ();
		double px, py;
		Path? p;
		Path path;

		if (button_down) {
			warning ("Discarding event.");
			return;
		}
				
		button_down = true;

		if (state == MOVE_HANDLES
			|| state == MOVE_LAST_HANDLE_RIGHT
			|| state == MOVE_LAST_HANDLE_LEFT) {
			return;	
		}
				
		if (b == 2) {
			if (g.is_open ()) {
				stop_drawing ();
				g.close_path ();
			} else {
				g.open_path ();
			}
			
			MainWindow.set_cursor (NativeWindow.VISIBLE);
			state = NONE;

			return;
		}

		// ignore double clicks
		if ((GLib.get_real_time () - last_press_time) / 1000000.0 < 0.2) {
			last_press_time = GLib.get_real_time ();
			return;
		}
		last_press_time = GLib.get_real_time ();
				
		g.store_undo_state ();
		
		PenTool.update_orientation ();
		
		MainWindow.set_cursor (NativeWindow.HIDDEN);
		g.open_path ();
		
		px = Glyph.path_coordinate_x (x);
		py = Glyph.path_coordinate_y (y);		

		if (GridTool.is_visible ()) {
			GridTool.tie_coordinate (ref px, ref py);
		}
		
		if (state == MOVE_HANDLE_ON_AXIS) {
			current_point = current_path.add (px, py);
			current_path.hide_end_handle = true;
			current_point.get_left_handle ().convert_to_line ();
			current_path.recalculate_linear_handles_for_point (current_point);
			set_point_type ();
			corner_node = false;
			state = MOVE_POINT;
		} else if (corner_node) {
			if (current_path.is_open ()) {
				current_point = current_path.add (px, py);
				current_path.hide_end_handle = true;
				current_point.get_left_handle ().convert_to_line ();
				current_path.recalculate_linear_handles_for_point (current_point);
				set_point_type ();
				g.clear_active_paths ();
				g.add_active_path (null, current_path);
				GlyphCanvas.redraw ();
				state = MOVE_POINT;
			} else {
				state = NONE;
			}
		} else if (state == NONE) {
			g.open_path ();
			current_path = new Path ();
			current_path.reopen ();
			current_path.hide_end_handle = true;
			current_point = current_path.add (px, py);
			current_point.get_left_handle ().convert_to_line ();
			current_path.recalculate_linear_handles_for_point (current_point);
			g.add_path (current_path);
			
			set_point_type ();
			
			if (StrokeTool.add_stroke) {
				current_path.stroke = StrokeTool.stroke_width;
				current_path.line_cap = StrokeTool.line_cap;
			}
			
			BirdFont.get_current_font ().touch ();
			
			GlyphCanvas.redraw ();
			state = MOVE_POINT;
		} else if (state == MOVE_POINT) {
			if (PenTool.can_join (current_point)) {
				EditPoint first = current_path.get_first_point ();
				
				p = PenTool.join_paths (current_point);
				return_if_fail (p != null);
				path = (!) p;
				swap = path.get_first_point () != first;
				
				if (current_path.points.size == 1) {
					return_if_fail (path.is_open ());
					current_path = path;
					current_point = path.get_last_point ();
					state = MOVE_POINT;
				} else {
					g.open_path ();
					current_path = path;
					current_point = !swap ? path.get_first_point () : path.get_last_point ();
					state = !swap ? MOVE_LAST_HANDLE_RIGHT : MOVE_LAST_HANDLE_LEFT;
				}
			} else {
				state = MOVE_HANDLES;
			}
		}
	}

	void set_point_type () {
		PointType pt;
		
		pt = DrawingTools.get_selected_point_type ();
		
		current_point.type = pt; 
		current_point.get_left_handle ().type = pt;
		current_point.get_right_handle ().type = pt;
		current_point.get_left_handle ().convert_to_line ();
		
		current_point.get_right_handle ().convert_to_line ();	
	}
	
	public void release (int b, int x, int y) {
		double px, py;
		Glyph g;
		
		if (!button_down) {
			warning ("Discarding event.");
			return;
		}
		
		button_down = false;
		
		if (state == NONE || state == MOVE_POINT) {
			return;
		}
		
		convert_zero_length_handles_to_lines ();	
		corner_node = false;
		
		// ignore double clicks
		if ((GLib.get_real_time () - last_release_time) / 1000000.0 < 0.2) {
			last_release_time = GLib.get_real_time ();
			return;
		}
		last_release_time = GLib.get_real_time ();
			
		px = Glyph.path_coordinate_x (x);
		py = Glyph.path_coordinate_y (y);

		if (GridTool.is_visible ()) {
			GridTool.tie_coordinate (ref px, ref py);
		}
		
		g = MainWindow.get_current_glyph ();
		
		if (state == MOVE_HANDLES) {
			current_point = current_path.add (px, py);
			current_path.hide_end_handle = true;
			current_point.get_left_handle ().convert_to_line ();
			current_path.recalculate_linear_handles_for_point (current_point);
			set_point_type ();
			g.clear_active_paths ();
			g.add_active_path (null, current_path);
			
			GlyphCanvas.redraw ();
			state = MOVE_POINT;
		} else if (state == MOVE_LAST_HANDLE_LEFT || state == MOVE_LAST_HANDLE_RIGHT) {
			current_path.update_region_boundaries ();
			g.close_path ();
			MainWindow.set_cursor (NativeWindow.VISIBLE);
			
			if (Path.is_counter (g.get_visible_path_list (), current_path)) {
				current_path.force_direction (Direction.COUNTER_CLOCKWISE);
			} else {
				current_path.force_direction (Direction.CLOCKWISE);
			}
						
			current_path.reset_stroke ();
			
			state = NONE;
		}
		
		convert_zero_length_handles_to_lines ();
	}
	
	void convert_zero_length_handles_to_lines () {
		EditPoint first, last;
		
		first = current_path.get_first_point ();

		first.tie_handles = false;
		first.reflective_point = false;

		if (first.get_left_handle ().length == 0) {
			first.get_left_handle ().length = 0.001;
		}
				
		if (first.get_right_handle ().length == 0) {
			first.get_right_handle ().convert_to_line ();
		}
		
		last = current_path.get_last_point ();

		last.tie_handles = false;
		last.reflective_point = false;

		if (last.get_left_handle ().length == 0) {
			last.get_left_handle ().length = 0.001;
		}
		
		if (last.get_right_handle ().length == 0) {
			last.get_right_handle ().convert_to_line ();
		}
	}
			
	public void move (int x, int y) {
		double px, py;
		
		last_x = x;
		last_y = y;
		
		px = Glyph.path_coordinate_x (x);
		py = Glyph.path_coordinate_y (y);	

		if (GridTool.is_visible ()) {
			GridTool.tie_coordinate (ref px, ref py);
		}
				
		if (state == MOVE_POINT) {
			current_point.x = px;
			current_point.y = py;
			current_path.hide_end_handle = true;
			current_path.recalculate_linear_handles_for_point (current_point);
			current_path.reset_stroke ();

			if (current_point.type == PointType.QUADRATIC) {
				current_path.create_list ();
				current_point.get_prev ().get_right_handle ().process_connected_handle ();
			}
				
			GlyphCanvas.redraw ();
		} else if (state == MOVE_HANDLES 
			|| state == MOVE_LAST_HANDLE_LEFT
			|| state == MOVE_LAST_HANDLE_RIGHT) {
				
			current_path.hide_end_handle = false;
			
			if (!corner_node) {
				current_point.set_reflective_handles (true);
				current_point.convert_to_curve ();
			}
			
			if (state == MOVE_LAST_HANDLE_LEFT) {
				current_point.get_left_handle ().move_to_coordinate (px, py);
			} else {
				current_point.get_right_handle ().move_to_coordinate (px, py);
			}
			
			current_path.reset_stroke ();
			GlyphCanvas.redraw ();
		} else if (state == MOVE_HANDLE_ON_AXIS) {
			EditPointHandle h = current_point.get_right_handle ();

			current_path.hide_end_handle = false;
			current_point.set_reflective_handles (true);
			current_point.convert_to_curve ();
			
			double tied_x = 0;
			double tied_y = 0;
			 
			PointTool.tie_angle (h.parent.x, h.parent.y,
					px, py, out tied_x, out tied_y);
					
			h.x = tied_x;
			h.y = tied_y;
			
			current_path.reset_stroke ();
			GlyphCanvas.redraw ();
		}
		
		if (current_path.points.size > 0) {
			current_path.get_first_point ().set_reflective_handles (false);
			current_path.get_last_point ().set_reflective_handles (false);
		}
		
		convert_zero_length_handles_to_lines ();
	}
	
	public void switch_to_line_mode () {
		int s = current_path.points.size;
		EditPoint p;
		
		if (s > 2) {
			p = current_path.points.get (s - 2);
			p.set_tie_handle (false);
			p.set_reflective_handles (false);
			p.get_right_handle ().convert_to_line ();
			current_point.get_left_handle ().convert_to_line ();
			current_path.recalculate_linear_handles_for_point (p);
			current_path.recalculate_linear_handles_for_point (current_point);
			current_path.reset_stroke ();
			GlyphCanvas.redraw ();
			
			state = MOVE_POINT;
		}
	}
	
	public void stop_drawing () {
		if (state == MOVE_POINT
			&& current_path.points.size > 0
			&& current_path.is_open ()) {
				
			current_path.delete_last_point ();
			current_path.reset_stroke ();
			current_path.create_full_stroke (); // cache better stroke
		}
		
		state = NONE;
	}

	public override void before_undo () {
	}

	public override void after_undo () {
		if (state != NONE) {
			MainWindow.set_cursor (NativeWindow.VISIBLE);
			state = NONE;
		}
	}
	
	public void create_corner () {
		Glyph g = MainWindow.get_current_glyph ();
		
		corner_node = true;
		g.open_path ();
		
		if (current_path.is_open ()) {
			current_path.delete_last_point ();
			current_path.reset_stroke ();
			current_point = current_path.get_last_point ();
			current_point.set_tie_handle (false);
			current_point.set_reflective_handles (false);
			state = MOVE_HANDLES;
		} else {
			state = swap ? MOVE_LAST_HANDLE_RIGHT : MOVE_LAST_HANDLE_LEFT;
		}
		
		current_point.set_reflective_handles (false);
		current_point.get_right_handle ().convert_to_curve ();
	}
	
	public void move_handle_on_axis () {
		state = MOVE_HANDLE_ON_AXIS;
	}
}

}

Contact

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