Clone in a terminal:
EditPoint.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/>.
*/

using Math;

namespace Supplement {

public enum PointType {
	NONE,
	NORMAL,
	LINE,
	CURVE,
	DOUBLE_CURVE,
	END,
	FLOATING
}

public class EditPoint {
	
	public double x;
	public double y;
	public PointType type;
	
	public double r = 1;
	public double g = 0;
	public double b = 0;
	public double a = 1;
	
	public unowned List<EditPoint>? prev = null;
	public unowned List<EditPoint>? next = null;

	public bool active = false;
	public bool selected = false;
		
	public int selected_handle = 0;
	
	public EditPointHandle right_handle;
	public EditPointHandle left_handle;
	
	public bool tie_handles = false;
	
	public EditPoint (double nx = 0, double ny = 0, PointType nt = PointType.LINE) {
		x = nx;
		y = ny;
		type = nt;
		active = false;
		
		set_active (true);
		
		if (nt == PointType.FLOATING) {
			r = 1;
			g = 1;
			b = 0;
			a = 1;
			active = 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;
		}
	}

	/** 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 || ((!)next).length () < 2) {
				return;
		}
		
		px = get_next ().nth (1).data.x;
		py = get_next ().nth (1).data.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;
		}		
	}
		
	/** Set bezier points for linear paths. */
	public void recalculate_linear_handles () {
		unowned EditPointHandle h;
		unowned EditPoint n;
		double nx, ny;

		if (prev == null && next != null) {
			prev = get_next ().last ();
		}

		// left handle
		if (prev != null) {
			n = get_prev ().data;
			h = get_left_handle ();
			
			if (h.type == PointType.LINE) {
				nx = x + ((n.x - x) / 3);
				ny = y + ((n.y - y) / 3);
			
				h.move_to_coordinate (nx, ny);
			}

			h = n.get_right_handle ();
			
			// on the other side
			h = n.get_right_handle ();
			
			if (h.type == PointType.LINE) {
				nx = n.x + ((x - n.x) / 3);
				ny = n.y + ((y - n.y) / 3);
			
				h.move_to_coordinate (nx, ny);
			}
		}

		// right handle
		if (next != null) {
			n = get_next ().data;
			h = get_right_handle ();
			
			if (h.type == PointType.LINE) {
				nx = x + ((n.x - x) / 3);
				ny = y + ((n.y - y) / 3);
				
				h.move_to_coordinate (nx, ny);
			}

			h = n.get_left_handle ();
			
			if (h.type == PointType.LINE) {
				nx = n.x + ((x - n.x) / 3);
				ny = n.y + ((y - n.y) / 3);

				h.move_to_coordinate (nx, ny);
			}
		}
	}
	
	public void set_tie_handle (bool t) {
		tie_handles = t;
	}

	public void process_tied_handle () {
		double a, b, c, length, angle;
		EditPointHandle eh;
		
		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;
		}
		
		left_handle.type = PointType.CURVE;
		right_handle.type = PointType.CURVE;
		
		right_handle.angle = angle;
		left_handle.angle = angle;

		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.set_color (r, g, b, a);
		
		new_point.tie_handles = tie_handles;
		
		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;

		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 () {
		return left_handle;
	}
	
	public unowned EditPointHandle get_right_handle () {
		return right_handle;
	}
	
	public void set_color (double r, double g, double b, double a) {
		this.r = r;
		this.g = g;
		this.b = b;
		this.a = a;
	}
	
	public unowned List<EditPoint> get_prev () {
		if (prev == null && next != null && get_next ().prev == get_next ().first ()) {
			prev = get_next ().last ();
		}
		
		if (unlikely (prev == null)) {
			warning ("EditPoint.prev is null");
		}
		
		return (!) prev;
	}

	public unowned List<EditPoint> get_next () {
		if (unlikely (next == null)) {
			warning ("EditPoint.next is null");
		}
		
		return (!) next;
	}
	
	public unowned List<EditPoint> get_list () {
		return get_next ().first ();
	}
	
	public void set_position (double tx, double ty) {
		x = tx;
		y = ty;

		if (unlikely (tx.is_nan () || ty.is_nan ())) {
			warning (@"Invalid point at ($tx,$ty).");
			x = 0;
			y = 0;
		}
	}
	
	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;
	}
	
	public void set_selected (bool s) {
		selected = s;
	}
	
	public bool set_active (bool active) {
		bool update = (this.active != active);
		
		if (update) {
			this.active = active;
		
			if (active) {
				r = 0.5;
				g = 0;
				b = 1;
				a = 1;
			} else {
				r = 0;
				g = 1;
				b = 0.5;
				a = 1;
			}
		}
		
		return update;
	}
	
	public int get_index () {
		int i = -1;
		
		if (next == null) return i;
	
		foreach (var d in get_next ().first ()) {
			i++;
			
			if (this == d) {
				return i;
			}
		}
		
		return i;
	}
}

}

Contact

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