Clone in a terminal:
ResizeTool.vala
/*
	Copyright (C) 2013 2015 2019 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 {

public class ResizeTool : Tool {
	bool resize_path_proportional = false;
	bool resize_width = false;

	Path? resized_path = null;
	double last_resize_y;
	double last_resize_x;

	bool move_paths = false;

	static double selection_box_width = 0;
	static double selection_box_height = 0;
	static double selection_box_center_x = 0;
	static double selection_box_center_y = 0;

	static bool rotate_path = false;
	static double last_rotate_y;
	static double rotation = 0;
	static double last_rotate = 0;
	
	public double last_skew = 0;
	
	public signal void objects_rotated (double angle);
	public signal void objects_resized (double width, double height);
	
	Text proportional_handle;
	Text horizontal_handle;
	
	public ResizeTool (string n) {
		base (n, t_("Resize and rotate paths"));

		proportional_handle = new Text ("resize_handle", 60);
		proportional_handle.load_font ("icons.birdfont");
		Theme.text_color (proportional_handle, "Highlighted 1");

		horizontal_handle = new Text ("resize_handle_horizontal", 60);
		horizontal_handle.load_font ("icons.birdfont");
		Theme.text_color (horizontal_handle, "Highlighted 1");
	
		select_action.connect((self) => {
		});

		deselect_action.connect((self) => {
		});
				
		press_action.connect((self, b, x, y) => {
			Path last_path;
			Glyph glyph;
			
			glyph = MainWindow.get_current_glyph ();
			glyph.store_undo_state ();
			
			foreach (Path p in glyph.active_paths) {
				if (is_over_resize_handle (p, x, y)) {
					resize_path_proportional = true;
					resized_path = p;
					last_resize_x = x;
					last_resize_y = y;
					return;
				}

				if (is_over_horizontal_resize_handle (p, x, y)) {
					resize_width = true;
					resized_path = p;
					last_resize_x = x;
					last_resize_y = y;
					return;					
				}
			}

			foreach (Path p in glyph.active_paths) {
				if (is_over_rotate_handle (p, x, y)) {
					rotate_path = true;
					return;
				}
			}

			if (glyph.active_paths.size > 0) {
				last_path = glyph.active_paths.get (glyph.active_paths.size - 1);
				last_rotate = last_path.rotation;
			}
					
			rotation = last_rotate;
			last_resize_x = x;
			last_rotate_y = y;
			
			if (!resize_path_proportional && !resize_width && !rotate_path) {
				DrawingTools.move_tool.press (b, x, y);
			}
			
			move_paths = true;
			
			update_selection_box ();
		});

		release_action.connect((self, b, x, y) => {
			resize_path_proportional = false;
			resize_width = false;
			rotate_path = false;
			move_paths = false;
			DrawingTools.move_tool.release (b, x, y);
			update_selection_box ();
			GlyphCanvas.redraw ();
			
			foreach (Path p in MainWindow.get_current_glyph ().active_paths) {
				p.create_full_stroke ();
			}
		});
		
		move_action.connect ((self, x, y)	 => {
			Glyph glyph;
			
			if (resize_path_proportional && can_resize (x, y)) {
				resize_proportional (x, y);
				update_selection_box ();
			}

			if (resize_width && can_resize (x, y)) {
				resize_horizontal (x, y);
				update_selection_box ();
			}

			if (rotate_path) {
				rotate (x, y);
				update_selection_box ();
			}

			if (move_paths 
				|| rotate_path
				|| resize_path_proportional
				|| resize_width) {
				
				glyph = MainWindow.get_current_glyph ();
				
				foreach (Path selected_path in glyph.active_paths) {
					selected_path.reset_stroke ();
				}
				
				GlyphCanvas.redraw ();
			}
			
			DrawingTools.move_tool.move (x, y);
		});
		
		draw_action.connect ((self, cr, glyph) => {
			Text handle;
			Glyph g = MainWindow.get_current_glyph ();
			
			if (!rotate_path) {
				if (!resize_width) {
					handle = proportional_handle;
					get_resize_handle_position (out handle.widget_x, out handle.widget_y);
					
					handle.widget_x -= handle.get_sidebearing_extent () / 2;
					handle.widget_y -= handle.get_height () / 2;
					
					handle.draw (cr);
				} 
				
				if (!resize_path_proportional) {
					handle = horizontal_handle;
					
					get_horizontal_reseize_handle_position (out handle.widget_x, 
						out handle.widget_y);
					
					handle.widget_x -= handle.get_sidebearing_extent () / 2;
					handle.widget_y -= handle.get_height () / 2;
					
					handle.draw (cr);
				}
			}

			if (!resize_path_proportional && !resize_width 
				&& g.active_paths.size > 0) {
				
				draw_rotate_handle (cr);
			}
		
			MoveTool.draw_actions (cr);
		});
		
		key_press_action.connect ((self, keyval) => {
			DrawingTools.move_tool.key_down (keyval);
		});
	}

	public static void get_resize_handle_position (out double px, out double py) {
		px = Glyph.reverse_path_coordinate_x (selection_box_center_x + selection_box_width / 2);
		py = Glyph.reverse_path_coordinate_y (selection_box_center_y + selection_box_height / 2);
	}

	public static void get_horizontal_reseize_handle_position (out double px, out double py) {
		px = Glyph.reverse_path_coordinate_x (selection_box_center_x + selection_box_width / 2);
		px += 40;
		py = Glyph.reverse_path_coordinate_y (selection_box_center_y);
	}

	public static double get_rotated_handle_length () {
		double s, hx, hy;
		double d;

		s = fmin (selection_box_width, selection_box_height) * 1.1;
		d = (s / Glyph.ivz ()) / 2;

		hx = cos (rotation) * d;
		hy = sin (rotation) * d;
		
		return d;
	}

	public void signal_objects_rotated () {
		objects_rotated (rotation * (180 / PI));
	}

	public void rotate_selected_paths (double angle, double cx, double cy) {
		Glyph glyph = MainWindow.get_current_glyph ();  
		double dx, dy, xc2, yc2, w, h;
		Path last_path;
		
		foreach (Path p in glyph.active_paths) {
			p.rotate (angle, cx, cy);
		}

		MoveTool.get_selection_box_boundaries (out xc2, out yc2, out w, out h); 

		dx = -(xc2 - cx);
		dy = -(yc2 - cy);
		
		foreach (Path p in glyph.active_paths) {
			p.move (dx, dy);
		}
		
		last_rotate = rotation;
		
		MoveTool.update_selection_boundaries ();
		
		if (glyph.active_paths.size > 0) {
			last_path = glyph.active_paths.get (glyph.active_paths.size - 1);
			rotation = last_path.rotation;
			
			if (rotation > PI) {
				rotation -= 2 * PI;
			}
			
			last_rotate = rotation;
			signal_objects_rotated ();
		}
	}
	
	/** Move rotate handle to pixel x,y. */
	void rotate (double x, double y) {
		double cx, cy, xc, yc, a, b;		
		
		cx = Glyph.reverse_path_coordinate_x (selection_box_center_x);
		cy = Glyph.reverse_path_coordinate_y (selection_box_center_y);
		xc = selection_box_center_x;
		yc = selection_box_center_y;
		
		a = x - cx;
		b = y - cy;
		
		rotation = atan (b / a);
		
		if (a < 0) {
			rotation += PI;
		}
		
		rotate_selected_paths (rotation - last_rotate, selection_box_center_x, selection_box_center_y);
	}

	static bool is_over_rotate_handle (Path p, double x, double y) {
		double cx, cy, hx, hy;
		double size = 10;
		bool inx, iny;
		
		cx = Glyph.reverse_path_coordinate_x (selection_box_center_x);
		cy = Glyph.reverse_path_coordinate_y (selection_box_center_y);

		hx = cos (rotation) * get_rotated_handle_length ();
		hy = sin (rotation) * get_rotated_handle_length ();

		inx = x - size <= cx + hx - 2.5 <= x + size;
		iny = y - size <= cy + hy - 2.5 <= y + size;
		
		return inx && iny;
	}
	
	static void draw_rotate_handle (Context cr) {
		double cx, cy, hx, hy;
		
		cx = Glyph.reverse_path_coordinate_x (selection_box_center_x);
		cy = Glyph.reverse_path_coordinate_y (selection_box_center_y);
		
		cr.save ();
		Theme.color (cr, "Highlighted 1");
		cr.rectangle (cx - 2.5, cy - 2.5, 5, 5);
		cr.fill ();

		hx = cos (rotation) * get_rotated_handle_length ();
		hy = sin (rotation) * get_rotated_handle_length ();
	
		cr.set_line_width (1);
		cr.move_to (cx, cy);
		cr.line_to (cx + hx, cy + hy);
		cr.stroke ();

		Theme.color (cr, "Highlighted 1");
		cr.rectangle (cx + hx - 2.5, cy + hy - 2.5, 5, 5);
		cr.fill ();
					
		cr.restore ();
	}
	
	double get_resize_ratio (double px, double py) {
		double ratio, x, y, w, h;
		
		Glyph glyph = MainWindow.get_current_glyph ();
		glyph.selection_boundaries (out x, out y, out w, out h);
		
		ratio = 1;
		
		if (Math.fabs (last_resize_y - py) > Math.fabs (last_resize_x - px)) {
			ratio = 1 + (Glyph.path_coordinate_y (py) 
				- Glyph.path_coordinate_y (last_resize_y)) / h;
		} else {
			ratio = 1 + (Glyph.path_coordinate_x (px) 
				- Glyph.path_coordinate_x (last_resize_x)) / w;
		}
		
		return ratio;
	}

	public void resize_selected_paths (double ratio_x, double ratio_y) {
		Glyph g = MainWindow.get_current_glyph ();
		resize_glyph (g, ratio_x, ratio_y, true, true);
	}

	public void resize_glyph (Glyph glyph,
			double ratio_x,
			double ratio_y,
			bool selected = true,
			bool relative_to_object = false) {
				
		Font font = BirdFont.get_current_font ();
		
		if (!selected) {
			glyph.clear_active_paths ();
			
			foreach (Path path in glyph.get_visible_paths ()) {
				glyph.add_active_path (null, path);
			}
		}
		
		foreach (Path path in glyph.active_paths) {
			x = selection_box_center_x - selection_box_width / 2;
			y = font.base_line;
				
			if (relative_to_object) {
				y = selection_box_center_y - selection_box_height / 2;
			}
				
			SvgTransforms transform = new SvgTransforms ();
			transform.resize (ratio_x, ratio_y, x, y);
			Matrix matrix = transform.get_matrix ();
			path.transform (matrix);
			path.reset_stroke ();
		}
				
		if (glyph.active_paths.size > 0) {
			foreach (Path p in glyph.active_paths) {
				p.update_region_boundaries ();
			}
		}

		if (!selected) {
			glyph.add_help_lines ();
			glyph.left_limit *= ratio_x;
			glyph.right_limit *= ratio_x;
			glyph.clear_active_paths ();
			glyph.remove_lines ();
			glyph.add_help_lines ();

			glyph.view_zoom = 1;
			glyph.view_offset_x = 0;
			glyph.view_offset_y = 0;
		}
	}

	void update_selection_box () {
		MoveTool.update_boundaries_for_selection ();
		MoveTool.get_selection_box_boundaries (out selection_box_center_x,
				out selection_box_center_y, out selection_box_width,
				out selection_box_height);
	}

	/** Move resize handle to pixel x,y. */
	void resize_proportional (double px, double py) {
		double ratio;
		
		ratio = get_resize_ratio (px, py);
		
		if (ratio != 1) {
			resize_selected_paths (ratio, ratio);
			last_resize_x = px;
			last_resize_y = py;
		}
	}

	/** Move resize handle to pixel x,y. */
	void resize_horizontal (double px, double py) {
		double ratio, x, y, w, h;

		Glyph glyph = MainWindow.get_current_glyph ();
		glyph.selection_boundaries (out x, out y, out w, out h);
				
		ratio = 1 + (Glyph.path_coordinate_x (px) 
			- Glyph.path_coordinate_x (last_resize_x)) / w;
		
		if (ratio != 1) {
			resize_selected_paths (ratio, 1);
			last_resize_x = px;
			last_resize_y = py;
		}
	}
	
	public void full_height () {
		double xc, yc, w, h;
		Glyph glyph = MainWindow.get_current_glyph ();
		Font font = BirdFont.get_current_font ();
		
		MoveTool.update_boundaries_for_selection ();
		MoveTool.get_selection_box_boundaries (out xc, out yc, out w, out h);

		//compute scale
		double descender = font.base_line - (yc - h / 2);
		
		if (descender < 0) {
			descender = 0;
		}
		
		double font_height = font.top_position - font.base_line;
		double scale = font_height / (h - descender);
		
		resize_selected_paths (scale, scale);
		PenTool.reset_stroke ();

		MoveTool.update_boundaries_for_selection ();
		font.touch ();

		MoveTool.get_selection_box_boundaries (out selection_box_center_x,
											   out selection_box_center_y,
											   out selection_box_width,
											   out selection_box_height);
		
		DrawingTools.move_tool.move_to_baseline ();

		foreach (Path path in glyph.active_paths) {
			path.move (0, -descender * scale);
		}
		
		objects_resized (selection_box_width, selection_box_height);
	}
    /*
	void get_selection_min (out double x, out double y) {
		Glyph glyph = MainWindow.get_current_glyph ();
		x = double.MAX;
		y = double.MAX;
		foreach (Path p in glyph.active_paths) {			
			if (p.xmin < x) {
				x = p.xmin;
			}
			
			if (p.ymin < y) {
				y = p.ymin;
			}
		}
	}
    */
	bool can_resize (double x, double y) {
		Glyph glyph = MainWindow.get_current_glyph ();
		double h, w;
		double ratio = get_resize_ratio (x, y);
		
		foreach (Path selected_path in glyph.active_paths) {
			h = selected_path.ymax - selected_path.ymin;
			w = selected_path.xmax - selected_path.xmin;
			
			if (selected_path.points.size <= 1) {
				continue;
			}
			
			if (h * ratio < 0.0001 || w * ratio < 0.0001) {
				return false;
			}
		}
		
		return true;
	}

	bool is_over_resize_handle (Path p, double x, double y) {
		double handle_x, handle_y;
		get_resize_handle_position (out handle_x, out handle_y);
		return Path.distance (handle_x, x, handle_y, y) < 12;
	}

	bool is_over_horizontal_resize_handle (Path p, double x, double y) {
		double handle_x, handle_y;
		get_horizontal_reseize_handle_position (out handle_x, out handle_y);
		return Path.distance (handle_x, x, handle_y, y) < 12;		
	}

	public void skew (double skew) {
		Glyph glyph = MainWindow.get_current_glyph ();
		skew_glyph (glyph, skew, last_skew, true);
		last_skew = skew;
	}
	
	public void skew_glyph (Glyph glyph, double skew, double last_skew,
		bool selected_paths) {
		
		double dx, nx, nw, dw, x, y, w, h;
		double s = (skew - last_skew) / 100.0;

		if (!selected_paths) {
			glyph.clear_active_paths ();
			
			foreach (Path path in glyph.get_visible_paths ()) {
				glyph.add_active_path (null, path);
			}
		}

		glyph.selection_boundaries (out x, out y, out w, out h);
	
		foreach (Path path in glyph.active_paths) {
			SvgParser.apply_matrix (path, 1, 0, s, 1, 0, 0);
			path.skew = skew;
			path.update_region_boundaries ();
		}
		
		glyph.selection_boundaries (out nx, out y, out nw, out h);
		
		dx = -(nx - x);
		
		foreach (Path p in glyph.active_paths) {
			p.move (dx, 0);
			p.reset_stroke ();
		}
		
		dw = (nw - w);
		glyph.right_limit += dw;
		glyph.remove_lines ();
		glyph.add_help_lines ();		

		if (!selected_paths) {
			glyph.clear_active_paths ();
		}
	}
}

}

Contact

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