Clone in a terminal:
EditPoint.vala
/*
	Copyright (C) 2012 2013 2014 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;

namespace BirdFont {

public enum PointType {
	NONE,
	LINE_QUADRATIC,      // line with quadratic handle
	LINE_DOUBLE_CURVE,   // line with two quadratic handles
	LINE_CUBIC,          // line with cubic handles
	CUBIC,
	DOUBLE_CURVE,        // two quadratic points with a hidden point half way between the two line handles
	QUADRATIC,
	HIDDEN,
	FLOATING,
	END
}

public class EditPoint : GLib.Object {
	
	public double x;
	public double y;
	public PointType type;
	
	public unowned EditPoint? prev = null;
	public unowned EditPoint? next = null;

	public const uint NONE = 0;
	public const uint ACTIVE = 1;
	public const uint SELECTED = 1 << 1;
	public const uint DELETED_POINT = 1 << 2;
	public const uint TIE = 1 << 3;
	public const uint REFLECTIVE = 1 << 4;
	
	public const uint INTERSECTION = 1 << 5;
	public const uint NEW_CORNER = 1 << 6;
	public const uint STROKE_OFFSET = 1 << 7;
	public const uint COUNTER_TO_OUTLINE = 1 << 8;
	public const uint COPIED = 1 << 9;
	public const uint REMOVE_PART = 1 << 10;
	public const uint OVERLAY = 1 << 11;
	public const uint CURVE = 1 << 12;
	public const uint CURVE_KEEP = 1 << 13;
	public const uint SEGMENT_END = 1 << 14;
	public const uint SPLIT_POINT = 1 << 15;
	public const uint SELF_INTERSECTION = 1 << 16;
	public const uint COPIED_SELF_INTERSECTION = 1 << 17;
	
	public uint flags = NONE;
	
	public bool active_point {
		get {
			return (flags & ACTIVE) > 0;
		}
		
		set {
			if (value) {
				flags |= ACTIVE;
			} else {
				flags &= ~ACTIVE;
			}
		}	
	}
	
	public bool selected_point {
		get {
			return (flags & SELECTED) > 0;
		}
		
		set {
			if (value) {
				flags |= SELECTED;
			} else {
				flags &= ~SELECTED;
			}
		}	
	}
	
	public bool deleted {
		get {
			return (flags & DELETED_POINT) > 0;
		}
		
		set {
			if (value) {
				flags |= DELETED_POINT;
			} else {
				flags &= ~DELETED_POINT;
			}
		}	
	}

	public bool tie_handles {
		get {
			return (flags & TIE) > 0;
		}
		
		set {
			if (value) {
				flags |= TIE;
			} else {
				flags &= ~TIE;
			}
		}	
	}

	public bool reflective_point {
		get {
			return (flags & REFLECTIVE) > 0;
		}
		
		set {
			if (value) {
				flags |= REFLECTIVE;
			} else {
				flags &= ~REFLECTIVE;
			}
		}	
	}
			
	public int selected_handle = 0;
	
	public EditPointHandle right_handle;
	public EditPointHandle left_handle;

	/** Set new position for control point without moving handles. */
	public double independent_x {
		get { 
			return x;
		}
		
		set { 
			double d = value - x;
			x = value;
			right_handle.independent_x -= d;
			left_handle.independent_x -= d;
		}
	}

	public double independent_y {
		get {
			return y;	
		}
		
		set { 
			double d = value - y;
			y = value;
			right_handle.independent_y -= d;
			left_handle.independent_y -= d;
		}
	}
	
	public Color? color = null;

	public EditPoint (double nx = 0, double ny = 0, PointType nt = PointType.NONE) {	
		x = nx;
		y = ny;
		type = nt;
		right_handle = new EditPointHandle (this, 0, 7);
		left_handle = new EditPointHandle (this, PI, 7);	
	}
	
	public EditPoint.full (double nx = 0, double ny = 0, PointType nt = PointType.NONE) {
		x = nx;
		y = ny;
		type = nt;
		active_point = true;
		
		if (nt == PointType.FLOATING) {
			active_point = false;
		}
	
		right_handle = new EditPointHandle (this, 0, 7);
		left_handle = new EditPointHandle (this, PI, 7);

		if (unlikely (nx.is_nan () || ny.is_nan ())) {
			warning (@"Invalid point at ($nx,$ny).");
			x = 0;
			y = 0;
		}		
	}
	
	public bool is_valid () {
		return is_valid_position (x, y);
	}

	public static bool is_valid_position (double x, double y) {
		return likely (x.is_finite () && y.is_finite () 
			&& x > Glyph.CANVAS_MIN && x < Glyph.CANVAS_MAX
			&& y > Glyph.CANVAS_MIN && y < Glyph.CANVAS_MAX);
	}

	public void set_point_type (PointType t) {
		type = t;
	}
		
	public bool equals (EditPoint e) {
		return e.x == x 
			&& e.y == y 
			&& get_right_handle ().x == e.get_right_handle ().x
			&& get_right_handle ().y == e.get_right_handle ().y
			&& get_left_handle ().x == e.get_left_handle ().x
			&& get_left_handle ().y == e.get_left_handle ().y;
	}

	/** Make handles symmetrical. */
	public void set_reflective_handles (bool symmetrical) {
		reflective_point = symmetrical;
		
		if (symmetrical) {
			get_left_handle ().convert_to_curve ();
			get_right_handle ().convert_to_curve ();
			process_tied_handle ();
		}
	}

	/** Flip handles if next point on path is in the other direction. 
	 *  Used to recalculate handles after new point is inserted on a path.
	 */
	public void recalculate_handles (double px, double py) {
		double dr, dl;
		EditPointHandle t;
		
		if (next == null || get_next ().next != null) {
				return;
		}

		if (unlikely (reflective_point || tie_handles)) {
			warning ("Points on lines can't have tied handles.");
			return;
		}
		
		px = get_next ().get_next ().x;
		py = get_next ().get_next ().y;
		
		dr = Math.sqrt (Math.pow (px - right_handle.x, 2) + Math.pow (py - right_handle.y, 2));
		dl = Math.sqrt (Math.pow (px - left_handle.x, 2) + Math.pow (py - left_handle.y, 2));

		// flip handles
		if (dl < dr) {
			t = right_handle;
			right_handle = left_handle;
			left_handle = t;
		}		
	}
		
	public bool is_clockwise () {
		return get_direction () >= 0;
	}
	
	public double get_direction () {
		if (prev == null) {
			return 0;
		}
		
		return (x - get_prev ().x) * (y + get_prev ().y);
	}
	
	public void set_tie_handle (bool tie) {
		tie_handles = tie;
	}
	
	public void to_curve () {
		convert_from_line_to_curve (get_right_handle ());
		convert_from_line_to_curve (get_left_handle ());
	}
	
	public static void convert_from_line_to_curve (EditPointHandle h) {
		switch (h.type) {
			case PointType.LINE_QUADRATIC:
				h.type = PointType.QUADRATIC;
				break;
			case PointType.LINE_DOUBLE_CURVE:
				h.type = PointType.DOUBLE_CURVE;
				break;
			case PointType.LINE_CUBIC:
				h.type = PointType.CUBIC;
				break;
			default:
				break;
		}
	}

	public void process_tied_handle () 
	requires (next != null && prev != null) {
		double a, b, c, length, angle;
		EditPointHandle eh;
		EditPointHandle prev_rh, next_lh;
		
		eh = right_handle;
		
		a = left_handle.x - right_handle.x;
		b = left_handle.y - right_handle.y;
		c = a * a + b * b;
		
		if (c == 0) {
			return;
		}
		
		length = sqrt (fabs (c));
		
		if (right_handle.y < left_handle.y) {
			angle = acos (a / length) + PI;
		} else {
			angle = -acos (a / length) + PI;
		}
		
		prev_rh = get_prev ().get_right_handle ();
		next_lh = get_next ().get_left_handle ();	
		
		convert_from_line_to_curve (next_lh);
		convert_from_line_to_curve (prev_rh);
		convert_from_line_to_curve (left_handle);
		convert_from_line_to_curve (right_handle);	
		
		right_handle.angle = angle;
		left_handle.angle = angle - PI;

		set_tie_handle (true);
		eh.move_to_coordinate (right_handle.x, right_handle.y);
	}

	public EditPoint copy () {
		EditPoint new_point = new EditPoint ();
		
		new_point.x = x;
		new_point.y = y;
		
		new_point.type = type;
		new_point.flags = flags;

		new_point.right_handle.angle = right_handle.angle;
		new_point.right_handle.length = right_handle.length;
		new_point.right_handle.type = right_handle.type;

		new_point.left_handle.angle = left_handle.angle;
		new_point.left_handle.length = left_handle.length;		
		new_point.left_handle.type = left_handle.type;
		
		new_point.color = color;
		
		return new_point;
	}

	public double get_distance (double x, double y) {
		return Path.distance (this.x, x, this.y, y);
	}

	public unowned EditPointHandle get_left_handle () {
		if (unlikely (is_null (left_handle))) {
			warning ("EditPoint.left_handle is null");
		}
		
		return left_handle;
	}
	
	public unowned EditPointHandle get_right_handle () {
		if (unlikely (is_null (right_handle))) {
			warning ("EditPoint.right_handle is null");
		}
		
		return right_handle;
	}
	
	public unowned EditPoint get_prev () {
		if (unlikely (prev == null)) {
			warning ("EditPoint.prev is null");
		}
		
		return (!) prev;
	}

	public unowned EditPoint get_next () {
		if (unlikely (next == null)) {
			warning ("EditPoint.next is null");
		}
		
		return (!) next;
	}
	
	public unowned EditPoint get_link_item () {
		return this;
	}
	
	public void set_independet_position (double tx, double ty) {
		double rx, ry, lx, ly;
		
		rx = right_handle.x;
		ry = right_handle.y;

		lx = left_handle.x;
		ly = left_handle.y;
				
		set_position (tx, ty);
		
		left_handle.move_to_coordinate (lx, ly);
		right_handle.move_to_coordinate (rx, ry);
	}
		
	public void set_position (double tx, double ty) {
		EditPoint p, n;
		
		x = tx;
		y = ty;
		
		if (unlikely (tx.is_nan () || ty.is_nan ())) {
			warning (@"Invalid point at ($tx,$ty).");
			x = 0;
			y = 0;
		}
		
		// move connected quadratic handle
		if (right_handle.type == PointType.QUADRATIC) {
			if (next != null) {
				n = get_next ();
				n.set_tie_handle (false);
				n.set_reflective_handles (false);
				n.left_handle.move_to_coordinate_internal (right_handle.x, right_handle.y);
			}
		}
		
		if (left_handle.type == PointType.QUADRATIC) {
			if (prev != null && !get_prev ().is_selected ()) {
				p = get_prev ();
				p.set_tie_handle (false);
				p.set_reflective_handles (false);
				p.right_handle.move_to_coordinate (left_handle.x, left_handle.y);
			}
		}
	}
	
	public static void to_coordinate (ref double x, ref double y) {
		double xc, yc, xt, yt, ivz;
		Glyph g = MainWindow.get_current_glyph ();
		
		ivz = 1 / g.view_zoom;
		
		xc = g.allocation.width / 2.0;
		yc = g.allocation.height / 2.0;
				
		x *= ivz;
		y *= ivz;

		xt = x - xc + g.view_offset_x;
		yt = yc - y - g.view_offset_y;		
		
		x = xt;
		y = yt;
	}
	
	public bool is_selected () {
		return selected_point;
	}
	
	public void set_selected (bool s) {
		selected_point = s;
	}
	
	public bool set_active (bool active) {
		bool update = (this.active_point != active);
		
		if (update) {
			this.active_point = active;
		}
		
		return update;
	}

	public void convert_to_line () {
		left_handle.convert_to_line ();
		right_handle.convert_to_line ();		
	}

	public void convert_to_curve () {
		left_handle.convert_to_curve ();
		right_handle.convert_to_curve ();		
	}
			
	public string to_string () {
		StringBuilder s = new StringBuilder ();
		
		if (deleted) {
			s.append (@"Deleted ");
		}
		
		s.append (@"Control point: $x, $y\n");
		s.append (@"Left handle: angle: $(left_handle.angle) l: $(left_handle.length)\n");
		s.append (@"Right handle: angle: $(right_handle.angle) l: $(right_handle.length)\n");
		s.append (@"Type: $type Left: $(left_handle.type) Right: $(right_handle.type)\n".replace ("BIRD_FONT_POINT_TYPE_", ""));
		s.append (@"Flags $(flags)\n");
		
		return s.str;
	}
}

}

Contact

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