Clone in a terminal:
TrackTool.vala
/*
	Copyright (C) 2014 2015 2016 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 Cairo;
using Math;

namespace BirdFont {

/** A tool that lets the user draw fonts with the mouse 
  * instead of adding bezér points one by one.
  */
public class TrackTool : Tool {
	
	bool draw_freehand = false;

	/** Number of points to take the average from in order to create a smooth shape. */
	int added_points = 0;
	
	/** The time in milliseconds when a point was added to the path.
	 * after a few milliseconds will this tool add a sharp corner instead of  
	 * a smooth curve if the user does not move the pointer.
	 */
	double last_update = 0;

	/** The position of mouse pointer. at lat update. */
	int last_x = 0;
	int last_y = 0;

	/** The position of the mouse pointer when this tool is checking if
	 * the pointer has moved.
	 */
	int last_timer_x = 0;
	int last_timer_y = 0;
	int update_cycles = 0;

	/** Join the stroke with the path at the end point in this coordinate. */
	int join_x = -1;
	int join_y = -1;
	bool join_paths = false;

	/** Adjust the number of samples per point by this factor. */
	double samples_per_point = 1;
	bool drawing = false;

	public TrackTool (string name) {
		base (name, t_("Freehand drawing"));
		
		select_action.connect (() => {
			convert_points_to_line ();
			draw_freehand = false;
		});

		deselect_action.connect (() => {
			convert_points_to_line ();
			draw_freehand = false;
		});
				
		press_action.connect ((self, button, x, y) => {
			Glyph glyph = MainWindow.get_current_glyph ();
			Path p;
			PointSelection? ps;
			PointSelection end_point;

			if (button == 3) {
				glyph.clear_active_paths ();
			}
						
			if (button == 2) {
				glyph.close_path ();
			}
			
			if (button == 1) {
				if (draw_freehand) {
					warning ("Already drawing.");
					return;
				}
				
				return_if_fail (!drawing);
				
				draw_freehand = true;
				
				last_x = x;
				last_y = y;
				
				glyph.store_undo_state ();
				
				if (join_paths) {
					ps = get_path_with_end_point (x, y);
					
					if (unlikely (ps == null)) {
						warning ("No end point.");
						return;
					}
					
					end_point = (!) ps;

					if (end_point.is_first ()) {
						end_point.path.reverse ();
					}
								
					glyph.set_active_path (end_point.path);
				} else {
					p = new Path ();
					glyph.add_path (p);
					glyph.open_path ();
					
					PenTool.add_new_edit_point (x, y);
				}

				glyph.update_view ();				
				added_points = 0;
				last_update = get_current_time ();
				start_update_timer ();
				drawing = true;

				if (is_null(glyph.active_paths)) {
					warning("No active paths in free hand tool.");
					return;				
				}
				
				foreach (Path path in glyph.active_paths) {
					path.create_full_stroke (); // cache merged stroke parts
				}
			}
		});
		
		double_click_action.connect ((self, b, x, y) => {
		});

		release_action.connect ((self, button, x, y) => {
			Path p;
			Glyph g = MainWindow.get_current_glyph ();
			EditPoint previous;

			if (button == 1) {
				if (!draw_freehand) {
					warning ("Not drawing.");
					return;
				}
				
				convert_points_to_line ();
				
				g = MainWindow.get_current_glyph ();
				
				return_if_fail (!is_null (g.active_paths));
				
				if (!is_null (g.active_paths) && g.active_paths.size > 0) { // set type for last point
					p = g.active_paths.get (g.active_paths.size - 1);

					return_if_fail (!is_null (p.points));
					
					if (!is_null (p.points) && p.points.size > 1) {
						previous = p.points.get (p.points.size - 1);
						previous.type = PointType.CUBIC;
						previous.set_tie_handle (false);

						previous = p.points.get (0);
						previous.type = PointType.CUBIC;
						previous.set_tie_handle (false);
					}
				}

				if (button == 1 && draw_freehand) {
					return_if_fail (drawing);
					add_endpoint_and_merge (x, y);
				}
				
				var paths_in_layer = g.layers.get_all_paths (); 

				if (is_null (paths_in_layer) || is_null (paths_in_layer)) {
					warning ("No layers in glyph.");		
				} else {
					foreach (Path path in paths_in_layer.paths) {
						convert_hidden_points (path);
						path.update_region_boundaries ();
					}				
				}	
				
				g.clear_active_paths ();
				
				set_tie ();
				PenTool.force_direction (); 
				PenTool.reset_stroke ();
				BirdFont.get_current_font ().touch ();
				drawing = false;
			}
		});

		move_action.connect ((self, x, y) => {
			PointSelection? open_path = get_path_with_end_point (x, y);
			PointSelection p;
			bool join;
			
			join = (open_path != null);
			
			if (join != join_paths) {
				MainWindow.get_current_glyph ().update_view ();
				PenTool.reset_stroke ();
			}
			
			join_paths = join;
			
			if (open_path != null) {
				p = (!) open_path;
				join_x = Glyph.reverse_path_coordinate_x (p.point.x);
				join_y = Glyph.reverse_path_coordinate_y (p.point.y);
			}
			
			if (draw_freehand) {
				record_new_position (x, y);
				convert_on_timeout ();
				last_x = x;
				last_y = y;
				PenTool.reset_stroke ();
			}
		});
		
		draw_action.connect ((tool, cairo_context, glyph) => {
			if (join_paths) {
				PenTool.draw_join_icon (cairo_context, join_x, join_y);
			}
		});
		
		key_press_action.connect ((self, keyval) => {
		});
	}
	
	void convert_hidden_points (Path p) {
		foreach (EditPoint e in p.points) {
			if (e.type == PointType.HIDDEN) {
				e.type = DrawingTools.point_type;
				e.get_right_handle ().type = PointType.CUBIC;
				e.get_left_handle ().type = PointType.CUBIC;
			}
		}
	}
	
	// FIXME: double check
	void set_tie () {
		Glyph glyph = MainWindow.get_current_glyph ();
		var paths = glyph.get_visible_paths ();
		
		return_if_fail (!is_null (paths));
		return_if_fail (paths.size > 0);
		
		Path p = paths.get (paths.size - 1);
		
		foreach (EditPoint ep in p.points) {
			if (ep.get_right_handle ().is_line () || ep.get_left_handle ().is_line ()) {
				ep.set_tie_handle (false);
			}

			if (!ep.get_right_handle ().is_line () || !ep.get_left_handle ().is_line ()) {
				ep.convert_to_curve ();
			}
		}
	}
	
	public void set_samples_per_point (double s) {
		samples_per_point = s;
	}

	void add_endpoint_and_merge (int x, int y) {
		Glyph glyph;
		Path p;
		PointSelection? open_path = get_path_with_end_point (x, y);
		PointSelection joined_path;
		
		glyph = MainWindow.get_current_glyph ();
		var paths = glyph.get_visible_paths ();
		
		if (paths.size == 0) {
			warning ("No path.");
			return;
		}
		
		// FIXME: double check this
		p = paths.get (paths.size - 1);
		draw_freehand = false;
		
		convert_points_to_line ();

		if (join_paths && open_path != null) {
			joined_path = (!) open_path;
			 
			if (joined_path.path == p) {
				delete_last_points_at (x, y);
				glyph.close_path ();
				p.close ();
			} else {
				p = merge_paths (p, joined_path);
				if (!p.is_open ()) {
					glyph.close_path ();
				}
			}
			
			glyph.clear_active_paths ();
		} else {
			add_corner (x, y);
		}

		if (p.points.size == 0) {
			warning ("No point.");
			return;
		}

		EditPoint last_point = p.get_last_point ();
		EditPointHandle handle = last_point.get_right_handle ();
		handle.convert_to_line ();
		p.recalculate_linear_handles ();

		PenTool.convert_point_type (p.get_last_point (), PointType.CUBIC);
		PenTool.convert_point_type (p.get_first_point (), PointType.CUBIC);

		p.create_list ();

		if (PenTool.is_counter_path (p)) {
			p.force_direction (Direction.COUNTER_CLOCKWISE);
		} else {
			p.force_direction (Direction.CLOCKWISE);
		}
				
		glyph.update_view ();
	}

	private static Path merge_paths (Path a, PointSelection b) {
		Glyph g;
		Path merged = a.copy ();

		if (a.points.size < 2) {
			warning ("Less than two points in path.");
			return merged;
		}

		if (b.path.points.size < 2) {
			warning ("Less than two points in path.");
			return merged;
		}
				
		if (!b.is_first ()) {
			b.path.close ();
			b.path.reverse ();
			b.path.reopen ();
		}
		
		merged.append_path (b.path);
		
		g = MainWindow.get_current_glyph ();		
		g.add_path (merged);
		a.delete_last_point ();
		
		update_corner_handle (a.get_last_point (), b.path.get_first_point ());
		
		g.delete_path (a);
		g.delete_path (b.path);
		
		merged.create_list ();
		merged.update_region_boundaries ();
		merged.recalculate_linear_handles ();
		merged.reopen ();

		return merged;
	}

	public static void update_corner_handle (EditPoint end, EditPoint new_start) {
		EditPointHandle h1, h2;
		
		h1 = end.get_right_handle ();
		h2 = new_start.get_left_handle ();
		
		h1.convert_to_line ();
		h2.convert_to_line ();
	}

	PointSelection? get_path_with_end_point (int x, int y) {
		Glyph glyph = MainWindow.get_current_glyph ();
		EditPoint e;
		EditPoint current_end = new EditPoint ();

		// exclude the end point on the path we are adding points to
		if (draw_freehand) {
			current_end = get_active_path ().get_last_point ();
		}

		foreach (Path p in glyph.get_visible_paths ()) {
			if (p.is_open () && p.points.size > 2) {
				e = p.points.get (0);
				if (PenTool.is_close_to_point (e, x, y)) {
					return new PointSelection (e, p);
				}

				e = p.points.get (p.points.size - 1);
				if (current_end != e && PenTool.is_close_to_point (e, x, y)) {
					return new PointSelection (e, p);				
				}
			}
		}
		
		return null;
	}
	
	void record_new_position (int x, int y) {
		Glyph glyph;
		Path p;
		EditPoint new_point;
		double px, py;
	
		glyph = MainWindow.get_current_glyph ();
		
		if (glyph.active_paths.size == 0) {
			warning ("No path.");
			return;
		}
		
		p = glyph.active_paths.get (glyph.active_paths.size - 1);
		p.reopen ();

		EditPoint last_point = new EditPoint ();
		
		if (p.points.size > 0) {
			last_point = p.get_last_point ();
		}
		
		px = Glyph.path_coordinate_x (x);
		py = Glyph.path_coordinate_y (y);
		
		new_point = new EditPoint (px, py, PointType.CUBIC);
		p.add_point (new_point);
		added_points++;

		PenTool.convert_point_to_line (new_point, false);
		new_point.set_point_type (PointType.HIDDEN);
		p.recalculate_linear_handles_for_point (new_point);

		last_point.get_right_handle ().length = 0.000001;		
		
		if (p.points.size > 1) {
			glyph.redraw_segment (new_point, new_point.get_prev ());
		}
		
		glyph.update_view ();
		
		last_x = x;
		last_y = y;
	}
	
	void start_update_timer () {
		TimeoutSource timer = new TimeoutSource (100);

		timer.set_callback (() => {
			if (draw_freehand) {
				record_new_position (last_x, last_y);
				convert_on_timeout ();
			}
					
			return draw_freehand;
		});

		timer.attach (null);
	}

	/** @returns true while the mounse pointer is moving. */
	bool is_moving (int x, int y) {
		return Path.distance (x, last_x, y, last_y) >= 1;
	}
	
	/** Add a new point if the update period has ended. */
	void convert_on_timeout () {
		if (!is_moving (last_timer_x, last_timer_y)) {
			update_cycles++;
		} else {
			last_timer_x = last_x;
			last_timer_y = last_y;
			update_cycles = 0;
		}
		
		if (update_cycles > 0.7 * 10) { // delay in time
			convert_points_to_line ();
			last_update = get_current_time ();
			add_corner (last_x, last_y);
			added_points = 0;
			update_cycles = 0;
		}
			
		if (added_points > 80 / samples_per_point) {
			last_update = get_current_time ();
			convert_points_to_line ();
		}
	}
	
	/** Add a sharp corner instead of a smooth curve. */
	void add_corner (double px, double py) {
		EditPoint p;
		p = new EditPoint (px, py, PointType.CUBIC);
		p.set_tie_handle (false);
		p.get_left_handle ().convert_to_line ();
		p.get_right_handle ().convert_to_line ();
		get_active_path ().recalculate_linear_handles_for_point (p);
		last_update = get_current_time ();
		MainWindow.get_current_glyph ().update_view ();
	}

	Path get_active_path () {
		Glyph glyph = MainWindow.get_current_glyph ();
		
		if (glyph.active_paths.size == 0) {
			warning ("No path.");
			return new Path ();
		}
			
		return glyph.active_paths.get (glyph.active_paths.size - 1);		
	}
	
	/** Delete all points close to the pixel at x,y. */
	void delete_last_points_at (double px, double py) {
		Path p;
					
		p = get_active_path ();

		if (unlikely (p.points.size == 0)) {
			warning ("Missing point.");
			return;
		}

		while (p.points.size > 0 && is_close (p.points.get (p.points.size - 1), px, py)) {
			p.delete_last_point ();
		}
	}
	
	/** @return true if the new point point is closer than a few pixels from p. */
	bool is_close (EditPoint p, double x, double y) {
		Glyph glyph = MainWindow.get_current_glyph ();
		return glyph.view_zoom * Path.distance (p.x, x, p.y, y) < 5;
	}
	
	/** Take the average of tracked points and create a smooth line.
	 * @return the last removed point.
	 */
	public void convert_points_to_line () {
		double sum_x, sum_y;
		Path p;
		Glyph glyph;
		Gee.ArrayList<EditPoint> points;
		
		points = new Gee.ArrayList<EditPoint> ();
		glyph = MainWindow.get_current_glyph ();
		var paths = glyph.get_visible_paths ();
		
		if (paths.size == 0) {
			warning ("No path.");
			return;
		}
			
		p = paths.get (paths.size - 1);
				
		if (added_points == 0) { // last point
			return;
		}

		if (unlikely (p.points.size < added_points)) {
			warning ("Missing point.");
			return;
		}

		sum_x = 0;
		sum_y = 0;
		
		int start = p.points.size - 1 - added_points;
		int stop = p.points.size - 1;
		
		EditPoint end = p.points.get (stop);
		
		Path segment = StrokeTool.fit_bezier_path (p, start, stop, 5.0 / samples_per_point);
		
		for (int i = 0; i < added_points; i++) {
			p.delete_last_point ();
		}		
		
		p.append_path (segment);
		p.remove_points_on_points ();
		
		add_corner (end.x, end.y);
		
		added_points = 0;
		last_update = get_current_time ();
		glyph.update_view ();
		p.reset_stroke ();
	}
	
	/** @return current time in milli seconds. */
	public static double get_current_time () {
		return GLib.get_real_time () / 1000.0;
	}
}

}

Contact

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