PenTool.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;
using Cairo;
namespace BirdFont {
/** Create new points. */
public class PenTool : Tool {
private static double contact_surface {
get {
return MainWindow.units * 20;
}
}
public static bool move_selected = false;
public static bool move_selected_handle = false;
public static bool move_point_on_path = false;
public static bool edit_active_corner = false;
public static bool move_point_independent_of_handle = false;
public static Gee.ArrayList<PointSelection> selected_points;
public static EditPointHandle active_handle;
public static EditPointHandle selected_handle;
public static PointSelection handle_selection;
public static EditPoint? active_edit_point;
public static Path active_path;
public static EditPoint selected_point;
public static Path selected_path;
public static double last_point_x = 0;
public static double last_point_y = 0;
public static bool show_selection_box = false;
private static double selection_box_x = 0;
private static double selection_box_y = 0;
private static double selection_box_last_x = 0;
private static double selection_box_last_y = 0;
private static bool point_selection_image = false;
public static double precision = 1;
// The pixel where the user pressed the mouse button
public static int begin_action_x = 0;
public static int begin_action_y = 0;
/* First move action must move the current point in to the grid. */
bool first_move_action = false;
/** Move curve handle instead of control point. */
private bool last_selected_is_handle = false;
static Gee.ArrayList<Path> clockwise;
static Gee.ArrayList<Path> counter_clockwise;
public static double path_stroke_width = 0;
public static double simplification_threshold = 0.5;
public static bool retain_angle = false;
/** Move handle along axis. */
public static bool on_axis = false;
public PenTool (string name) {
base (name, t_("Add new points"));
selected_points = new Gee.ArrayList<PointSelection> ();
active_handle = new EditPointHandle.empty ();
selected_handle = new EditPointHandle.empty ();
handle_selection = new PointSelection.empty ();
active_edit_point = new EditPoint ();
active_path = new Path ();
selected_path = new Path ();
selected_point = new EditPoint ();
clockwise = new Gee.ArrayList<Path> ();
counter_clockwise = new Gee.ArrayList<Path> ();
select_action.connect ((self) => {
});
deselect_action.connect ((self) => {
});
press_action.connect ((self, b, x, y) => {
// retain path direction
clockwise = new Gee.ArrayList<Path> ();
counter_clockwise = new Gee.ArrayList<Path> ();
begin_action_x = x;
begin_action_y = y;
update_orientation ();
first_move_action = true;
last_point_x = Glyph.path_coordinate_x (x);
last_point_y = Glyph.path_coordinate_y (y);
move_action (this, x, y);
press (b, x, y, false);
if (BirdFont.android) {
point_selection_image = true;
}
selection_box_x = x;
selection_box_y = y;
last_point_x = Glyph.path_coordinate_x (x);
last_point_y = Glyph.path_coordinate_y (y);
BirdFont.get_current_font ().touch ();
// move new points on to grid
if (b == 1 && (GridTool.has_ttf_grid () || GridTool.is_visible ())) {
move (x, y);
}
reset_stroke ();
});
double_click_action.connect ((self, b, x, y) => {
last_point_x = Glyph.path_coordinate_x (x);
last_point_y = Glyph.path_coordinate_y (y);
press (b, (int) x, (int) y, true);
});
release_action.connect ((self, b, ix, iy) => {
double x, y;
Glyph g;
g = MainWindow.get_current_glyph ();
x = Glyph.path_coordinate_x (ix);
y = Glyph.path_coordinate_y (iy);
if (has_join_icon () && active_edit_point != null) {
join_paths ((!) active_edit_point);
}
active_handle = new EditPointHandle.empty ();
if (show_selection_box) {
select_points_in_box ();
}
move_selected = false;
move_selected_handle = false;
edit_active_corner = false;
show_selection_box = false;
set_orientation ();
MainWindow.set_cursor (NativeWindow.VISIBLE);
point_selection_image = false;
BirdFont.get_current_font ().touch ();
foreach (Path p in g.get_visible_paths ()) {
if (p.is_open () && p.points.size > 0) {
p.get_first_point ().set_tie_handle (false);
p.get_first_point ().set_reflective_handles (false);
p.get_last_point ().set_tie_handle (false);
p.get_last_point ().set_reflective_handles (false);
}
// cache good stroke
if (p.full_stroke == null) {
p.create_full_stroke ();
}
}
// select or deselect points
if (KeyBindings.has_shift ()) {
control_point_event (x, y, false);
if (active_edit_point != null) {
if (((!)active_edit_point).is_selected () && selected_points.size > 1) {
((!)active_edit_point).set_selected (false);
} else {
((!)active_edit_point).set_selected (true);
}
update_selection ();
}
}
update_boundaries_for_selected_paths ();
on_axis = false;
});
move_action.connect ((self, x, y) => {
selection_box_last_x = x;
selection_box_last_y = y;
if (Path.distance (begin_action_x, x, begin_action_y, y) > 10) {
point_selection_image = false;
}
move (x, y);
});
key_press_action.connect ((self, keyval) => {
reset_stroke ();
if (keyval == Key.DEL || keyval == Key.BACK_SPACE) {
if (KeyBindings.has_shift ()) {
delete_selected_points ();
} else {
delete_simplify ();
}
}
if (is_arrow_key (keyval)) {
if (KeyBindings.modifier != CTRL) {
move_selected_points (keyval);
active_edit_point = selected_point;
} else {
move_select_next_point (keyval);
}
}
if (KeyBindings.has_shift ()) {
if (selected_point.tie_handles && KeyBindings.modifier == SHIFT) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
}
}
GlyphCanvas.redraw ();
BirdFont.get_current_font ().touch ();
});
key_release_action.connect ((self, keyval) => {
double x, y;
if (is_arrow_key (keyval)) {
if (KeyBindings.modifier != CTRL && active_edit_point != null) {
x = Glyph.reverse_path_coordinate_x (selected_point.x);
y = Glyph.reverse_path_coordinate_y (selected_point.y);
join_paths ((!) active_edit_point);
reset_stroke ();
}
}
});
draw_action.connect ((tool, cairo_context, glyph) => {
draw_on_canvas (cairo_context, glyph);
});
}
public static void update_orientation () {
Glyph glyph = MainWindow.get_current_glyph ();
clockwise.clear ();
counter_clockwise.clear ();
foreach (Path p in glyph.get_visible_paths ()) {
p.direction_is_set = true;
if (p.is_clockwise ()) {
clockwise.add (p);
} else {
counter_clockwise.add (p);
}
}
}
public static void set_orientation () {
// update path direction if it has changed
foreach (Path p in clockwise) {
if (!p.is_open () && !p.is_clockwise ()) {
p.reverse ();
update_selection ();
}
}
foreach (Path p in counter_clockwise) {
if (!p.is_open () && p.is_clockwise ()) {
p.reverse ();
update_selection ();
}
}
}
public bool has_join_icon () {
if (active_edit_point != null) {
return can_join ((!) active_edit_point);
}
return false;
}
public static bool can_join (EditPoint ep) {
double mx, my;
get_tie_position (ep, out mx, out my);
return (mx > -10 && my > -10);
}
public static void select_points_in_box () {
double x1, y1, x2, y2;
Glyph g;
g = MainWindow.get_current_glyph ();
x1 = Glyph.path_coordinate_x (fmin (selection_box_x, selection_box_last_x));
y1 = Glyph.path_coordinate_y (fmin (selection_box_y, selection_box_last_y));
x2 = Glyph.path_coordinate_x (fmax (selection_box_x, selection_box_last_x));
y2 = Glyph.path_coordinate_y (fmax (selection_box_y, selection_box_last_y));
if (!KeyBindings.has_shift ()) {
remove_all_selected_points ();
}
foreach (Path p in g.get_paths_in_current_layer ()) {
// TODO: Select path only of bounding box is in selection box
foreach (EditPoint ep in p.points) {
if (x1 <= ep.x <= x2 && y2 <= ep.y <= y1) {
add_selected_point (ep, p);
ep.set_selected (true);
}
}
}
}
public static void delete_selected_points () {
Glyph g = MainWindow.get_current_glyph ();
foreach (PointSelection p in selected_points) {
p.point.deleted = true;
}
process_deleted ();
foreach (Path p in g.get_all_paths ()) {
if (p.has_deleted_point ()) {
process_deleted ();
}
}
g.update_view ();
selected_points.clear ();
selected_handle.selected = false;
active_handle = new EditPointHandle.empty ();
selected_handle = new EditPointHandle.empty ();
active_edit_point = null;
selected_point = new EditPoint ();
}
static void get_closes_point_in_segment (EditPoint ep0, EditPoint ep1, EditPoint ep2,
double px, double py,
out double nx, out double ny) {
double npx0, npy0;
double npx1, npy1;
Path.find_closes_point_in_segment (ep0, ep1, px, py, out npx0, out npy0, 50);
Path.find_closes_point_in_segment (ep1, ep2, px, py, out npx1, out npy1, 50);
if (Path.distance (px, npx0, py, npy0) < Path.distance (px, npx1, py, npy1)) {
nx = npx0;
ny = npy0;
} else {
nx = npx1;
ny = npy1;
}
}
public static void get_path_distortion (EditPoint oe0, EditPoint oe1, EditPoint oe2,
EditPoint ep1, EditPoint ep2,
out double distortion_first, out double distortion_next) {
double nx, ny;
double df, dn;
int step;
df = 0;
dn = 0;
nx = 0;
ny = 0;
step = 4;
Path.all_of (ep1, ep2, (xa, ya, ta) => {
double f, n;
get_closes_point_in_segment (oe0, oe1, oe2, xa, ya, out nx, out ny);
if (ta < 0.5) {
f = Path.distance (nx, xa, ny, ya);
if (f > df) {
df += f;
}
} else {
n = Path.distance (nx, xa, ny, ya);
if (n > dn) {
dn += n;
}
}
return true;
}, step);
distortion_first = df;
distortion_next = dn;
}
public static void update_boundaries_for_selected_paths () {
Gee.ArrayList<Path> paths = new Gee.ArrayList<Path> ();
foreach (PointSelection p in selected_points) {
if (paths.index_of (p.path) == -1) {
paths.add (p.path);
}
}
foreach (Path p in paths) {
p.update_region_boundaries ();
}
}
public static void delete_simplify () {
Glyph g = MainWindow.get_current_glyph ();
foreach (PointSelection p in selected_points) {
remove_point_simplify (p);
p.path.reset_stroke ();
}
foreach (PointSelection p in selected_points) {
p.path.create_full_stroke ();
}
g.update_view ();
selected_points.clear ();
selected_handle.selected = false;
active_handle = new EditPointHandle.empty ();
selected_handle = new EditPointHandle.empty ();
active_edit_point = null;
selected_point = new EditPoint ();
}
/** @return path distortion. */
public static double remove_point_simplify (PointSelection p, double tolerance = 0.6) {
double e;
e = remove_point_simplify_path (p, tolerance, Glyph.CANVAS_MAX);
p.path.update_region_boundaries ();
return e;
}
/** @return path distortion. */
public static double remove_point_simplify_path (PointSelection p,
double tolerance = 0.6, double keep_tolerance = 10000) {
double start_length, stop_length;
double start_distortion, start_min_distortion, start_previous_length;
double stop_distortion, stop_min_distortion, stop_previous_length;
double distortion, min_distortion;
double prev_length_adjustment, next_length_adjustment;
double prev_length_adjustment_reverse, next_length_adjustment_reverse;
EditPoint ep1, ep2;
EditPoint next, prev;
double step, distance;
return_val_if_fail (p.path.points.size > 0, 0);
if (p.path.points.size <= 2) {
p.point.deleted = true;
p.path.remove_deleted_points ();
return 0;
}
p.point.deleted = true;
if (p.point.next != null) {
next = p.point.get_next ();
} else {
next = p.path.points.get (0);
}
if (p.point.prev != null) {
prev = p.point.get_prev ();
} else {
prev = p.path.points.get (p.path.points.size - 1);
}
prev.get_right_handle ().convert_to_curve ();
next.get_left_handle ().convert_to_curve ();
if (prev.get_right_handle ().type == PointType.QUADRATIC
&& next.get_left_handle ().type != PointType.QUADRATIC) {
convert_point_type (prev, next.get_left_handle ().type);
}
if (prev.get_right_handle ().type != PointType.QUADRATIC
&& next.get_left_handle ().type == PointType.QUADRATIC) {
convert_point_type (next, prev.get_right_handle ().type);
}
ep1 = prev.copy ();
ep2 = next.copy ();
start_length = ep1.get_right_handle ().length;
stop_length = ep2.get_left_handle ().length;
stop_previous_length = start_length;
start_previous_length = stop_length;
stop_min_distortion = double.MAX;
ep1.get_right_handle ().length = start_length;
start_min_distortion = double.MAX;
ep2.get_left_handle ().length = stop_length;
prev_length_adjustment = 0;
next_length_adjustment = 0;
prev_length_adjustment_reverse = 0;
next_length_adjustment_reverse = 0;
min_distortion = double.MAX;
distance = Path.distance (ep1.x, ep2.x, ep1.y, ep2.y);
for (double m = 50.0; m >= tolerance / 2.0; m /= 10.0) {
step = m / 10.0;
min_distortion = double.MAX;
double first = (m == 50.0) ? 0 : -m;
for (double a = first; a < m; a += step) {
for (double b = first; b < m; b += step) {
if (start_length + a + stop_length + b > distance) {
break;
}
ep1.get_right_handle ().length = start_length + a;
ep2.get_left_handle ().length = stop_length + b;
get_path_distortion (prev, p.point, next,
ep1, ep2,
out start_distortion, out stop_distortion);
distortion = Math.fmax (start_distortion, stop_distortion);
if (distortion < min_distortion
&& start_length + a > 0
&& stop_length + b > 0) {
min_distortion = distortion;
prev_length_adjustment_reverse = a;
next_length_adjustment = b;
}
}
}
start_length += prev_length_adjustment_reverse;
stop_length += next_length_adjustment;
}
if (min_distortion < keep_tolerance || keep_tolerance >= Glyph.CANVAS_MAX) {
prev.get_right_handle ().length = start_length;
if (prev.get_right_handle ().type != PointType.QUADRATIC) {
next.get_left_handle ().length = stop_length;
} else {
next.get_left_handle ().move_to_coordinate (
prev.get_right_handle ().x, prev.get_right_handle ().y);
}
p.point.deleted = true;
p.path.remove_deleted_points ();
}
return min_distortion;
}
/** @return path distortion. */
public static double remove_point_simplify_path_fast (PointSelection p,
double tolerance = 0.6, double keep_tolerance = 10000) {
double start_length, stop_length;
double start_distortion, start_min_distortion, start_previous_length;
double stop_distortion, stop_min_distortion, stop_previous_length;
double distortion, min_distortion;
double prev_length_adjustment, next_length_adjustment;
double prev_length_adjustment_reverse, next_length_adjustment_reverse;
EditPoint ep1, ep2;
EditPoint next, prev;
double step, distance;
return_val_if_fail (p.path.points.size > 0, 0);
if (p.path.points.size <= 2) {
p.point.deleted = true;
p.path.remove_deleted_points ();
return 0;
}
p.point.deleted = true;
if (p.point.next != null) {
next = p.point.get_next ();
} else {
next = p.path.points.get (0);
}
if (p.point.prev != null) {
prev = p.point.get_prev ();
} else {
prev = p.path.points.get (p.path.points.size - 1);
}
prev.get_right_handle ().convert_to_curve ();
next.get_left_handle ().convert_to_curve ();
if (prev.get_right_handle ().type == PointType.QUADRATIC
&& next.get_left_handle ().type != PointType.QUADRATIC) {
convert_point_type (prev, next.get_left_handle ().type);
}
if (prev.get_right_handle ().type != PointType.QUADRATIC
&& next.get_left_handle ().type == PointType.QUADRATIC) {
convert_point_type (next, prev.get_right_handle ().type);
}
ep1 = prev.copy ();
ep2 = next.copy ();
start_length = ep1.get_right_handle ().length;
stop_length = ep2.get_left_handle ().length;
stop_previous_length = start_length;
start_previous_length = stop_length;
stop_min_distortion = double.MAX;
ep1.get_right_handle ().length = start_length;
start_min_distortion = double.MAX;
ep2.get_left_handle ().length = stop_length;
prev_length_adjustment = 0;
next_length_adjustment = 0;
prev_length_adjustment_reverse = 0;
next_length_adjustment_reverse = 0;
min_distortion = double.MAX;
distance = Path.distance (ep1.x, ep2.x, ep1.y, ep2.y);
for (double m = 50.0; m >= tolerance / 2.0; m /= 10.0) {
step = m / 10.0;
min_distortion = double.MAX;
double first = (m == 50.0) ? 0 : -m;
for (double a = first; a < m; a += step) {
for (double b = first; b < m; b += step) {
if (start_length + a + stop_length + b > distance) {
break;
}
ep1.get_right_handle ().length = start_length + a;
ep2.get_left_handle ().length = stop_length + b;
get_path_distortion (prev, p.point, next,
ep1, ep2,
out start_distortion, out stop_distortion);
distortion = Math.fmax (start_distortion, stop_distortion);
if (distortion < min_distortion
&& start_length + a > 0
&& stop_length + b > 0) {
min_distortion = distortion;
prev_length_adjustment_reverse = a;
next_length_adjustment = b;
}
}
}
start_length += prev_length_adjustment_reverse;
stop_length += next_length_adjustment;
}
if (min_distortion < keep_tolerance || keep_tolerance >= Glyph.CANVAS_MAX) {
prev.get_right_handle ().length = start_length;
if (prev.get_right_handle ().type != PointType.QUADRATIC) {
next.get_left_handle ().length = stop_length;
} else {
next.get_left_handle ().move_to_coordinate (
prev.get_right_handle ().x, prev.get_right_handle ().y);
}
p.point.deleted = true;
p.path.remove_deleted_points ();
}
return min_distortion;
}
/** Retain selected points even if path is copied after running reverse. */
public static void update_selection () {
Glyph g = MainWindow.get_current_glyph ();
selected_points.clear ();
foreach (Path p in g.get_visible_paths ()) {
foreach (EditPoint e in p.points) {
if (e.is_selected ()) {
selected_points.add (new PointSelection (e, p));
}
}
}
}
static void process_deleted () {
Glyph g = MainWindow.get_current_glyph ();
while (g.process_deleted ());
}
public static void close_all_paths () {
Glyph g = MainWindow.get_current_glyph ();
foreach (Path p in g.get_visible_paths ()) {
if (p.stroke == 0) {
p.close ();
}
}
g.close_path ();
GlyphCanvas.redraw ();
}
public void set_precision (double p) {
precision = p;
SettingsDisplay.precision.set_value_round (p, false, false);
}
public static void reset_stroke () {
Glyph g = MainWindow.get_current_glyph ();
foreach (PointSelection p in selected_points) {
p.path.reset_stroke ();
}
foreach (Path path in g.active_paths) {
path.reset_stroke ();
}
}
public void move (int x, int y) {
double coordinate_x, coordinate_y;
double delta_coordinate_x, delta_coordinate_y;
double angle = 0;
bool tied;
Glyph g;
g = MainWindow.get_current_glyph ();
if (move_selected || move_selected_handle) {
cancel_stroke_creator ();
}
control_point_event (x, y);
curve_active_corner_event (x, y);
set_default_handle_positions ();
if (move_selected_handle && move_selected) {
warning ("move_selected_handle && move_selected");
move_selected = false;
move_selected_handle = false;
}
if (move_selected_handle || move_selected) {
MainWindow.set_cursor (NativeWindow.HIDDEN);
reset_stroke ();
} else {
MainWindow.set_cursor (NativeWindow.VISIBLE);
}
// move control point handles
if (move_selected_handle) {
set_type_for_moving_handle ();
// don't update angle if the user is holding down shift
if (KeyBindings.modifier == SHIFT || PenTool.retain_angle) {
angle = selected_handle.angle;
}
if (GridTool.is_visible ()) {
coordinate_x = Glyph.path_coordinate_x (x);
coordinate_y = Glyph.path_coordinate_y (y);
GridTool.tie_coordinate (ref coordinate_x, ref coordinate_y);
delta_coordinate_x = coordinate_x - last_point_x;
delta_coordinate_y = coordinate_y - last_point_y;
selected_handle.move_delta_coordinate (delta_coordinate_x, delta_coordinate_y);
} else if (GridTool.has_ttf_grid ()) {
coordinate_x = Glyph.path_coordinate_x (x);
coordinate_y = Glyph.path_coordinate_y (y);
GridTool.ttf_grid_coordinate (ref coordinate_x, ref coordinate_y);
delta_coordinate_x = coordinate_x - last_point_x;
delta_coordinate_y = coordinate_y - last_point_y;
selected_handle.move_delta_coordinate (delta_coordinate_x, delta_coordinate_y);
} else {
coordinate_x = Glyph.path_coordinate_x (x);
coordinate_y = Glyph.path_coordinate_y (y);
selected_handle.move_to_coordinate (coordinate_x, coordinate_y);
if (on_axis) {
double tied_x = 0;
double tied_y = 0;
PointTool.tie_angle (selected_handle.parent.x, selected_handle.parent.y,
coordinate_x, coordinate_y, out tied_x, out tied_y);
selected_handle.move_to_coordinate (tied_x, tied_y);
}
}
if (KeyBindings.modifier == SHIFT || PenTool.retain_angle) {
selected_handle.angle = angle;
selected_handle.process_connected_handle ();
if (selected_handle.parent.tie_handles) {
if (selected_handle.is_left_handle ()) {
selected_handle.parent.get_right_handle ().angle = angle - PI;
} else {
selected_handle.parent.get_left_handle ().angle = angle + PI;
}
}
}
handle_selection.path.update_region_boundaries ();
// FIXME: redraw line only
GlyphCanvas.redraw ();
if (GridTool.is_visible ()) {
last_point_x = selected_handle.x;
last_point_y = selected_handle.y;
} else if (GridTool.has_ttf_grid ()) {
last_point_x = selected_handle.x;
last_point_y = selected_handle.y;
} else {
last_point_x = Glyph.path_coordinate_x (x);
last_point_y = Glyph.path_coordinate_y (y);
}
}
// move edit point
if (move_selected && !show_selection_box) {
if (GridTool.is_visible ()) {
coordinate_x = Glyph.path_coordinate_x (x);
coordinate_y = Glyph.path_coordinate_y (y);
if (selected_point.tie_handles && KeyBindings.modifier == SHIFT) {
if (first_move_action) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
}
move_point_on_handles (coordinate_x, coordinate_y, out coordinate_x, out coordinate_y);
} else {
GridTool.tie_coordinate (ref coordinate_x, ref coordinate_y);
}
delta_coordinate_x = coordinate_x - last_point_x;
delta_coordinate_y = coordinate_y - last_point_y;
foreach (PointSelection selected in selected_points) {
if (move_point_independent_of_handle || KeyBindings.modifier == SHIFT) {
selected.point.set_independet_position (selected.point.x + delta_coordinate_x,
selected.point.y + delta_coordinate_y);
} else {
selected.point.set_position (selected.point.x + delta_coordinate_x,
selected.point.y + delta_coordinate_y);
}
selected.path.reset_stroke ();
selected.path.recalculate_linear_handles_for_point(selected.point);
selected.path.update_region_boundaries ();
}
} else if (GridTool.has_ttf_grid ()) {
coordinate_x = Glyph.path_coordinate_x (x);
coordinate_y = Glyph.path_coordinate_y (y);
GridTool.ttf_grid_coordinate (ref coordinate_x, ref coordinate_y);
if (selected_point.tie_handles && KeyBindings.modifier == SHIFT) {
if (first_move_action) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
}
move_point_on_handles (coordinate_x, coordinate_y, out coordinate_x, out coordinate_y);
}
delta_coordinate_x = coordinate_x - last_point_x;
delta_coordinate_y = coordinate_y - last_point_y;
foreach (PointSelection selected in selected_points) {
if (move_point_independent_of_handle || KeyBindings.modifier == SHIFT) {
tied = selected.point.tie_handles;
selected.point.tie_handles = false;
selected.point.set_independet_position (selected.point.x + delta_coordinate_x,
selected.point.y + delta_coordinate_y);
selected.point.tie_handles = tied;
} else {
selected.point.set_position (selected.point.x + delta_coordinate_x,
selected.point.y + delta_coordinate_y);
}
selected.path.reset_stroke ();
selected.path.recalculate_linear_handles_for_point(selected.point);
selected.path.update_region_boundaries ();
}
} else {
coordinate_x = Glyph.path_coordinate_x (x);
coordinate_y = Glyph.path_coordinate_y (y);
if (selected_point.tie_handles && KeyBindings.modifier == SHIFT) {
if (first_move_action) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
}
move_point_on_handles (coordinate_x, coordinate_y, out coordinate_x, out coordinate_y);
delta_coordinate_x = coordinate_x - last_point_x;
delta_coordinate_y = coordinate_y - last_point_y;
} else {
delta_coordinate_x = coordinate_x - last_point_x;
delta_coordinate_y = coordinate_y - last_point_y;
}
foreach (PointSelection selected in selected_points) {
if (move_point_independent_of_handle || KeyBindings.modifier == SHIFT) {
selected.point.set_independet_position (selected.point.x + delta_coordinate_x,
selected.point.y + delta_coordinate_y);
} else {
selected.point.set_position (selected.point.x + delta_coordinate_x,
selected.point.y + delta_coordinate_y);
}
selected.path.recalculate_linear_handles_for_point(selected.point);
selected.path.reset_stroke ();
selected.path.update_region_boundaries ();
}
}
if (selected_point.tie_handles && KeyBindings.modifier == SHIFT) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
} else if (GridTool.is_visible ()) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
} else if (GridTool.has_ttf_grid ()) {
last_point_x = selected_point.x;
last_point_y = selected_point.y;
} else {
last_point_x = Glyph.path_coordinate_x (x);
last_point_y = Glyph.path_coordinate_y (y);
}
GlyphCanvas.redraw ();
}
if (show_selection_box) {
GlyphCanvas.redraw ();
}
}
static void cancel_stroke_creator () {
foreach (PointSelection p in selected_points) {
p.path.stop_stroke_creator ();
}
}
private void move_point_on_handles (double px, double py, out double cx, out double cy) {
EditPoint ep;
ep = selected_point.copy ();
ep.tie_handles = false;
ep.reflective_point = false;
ep.get_right_handle ().angle += PI / 2;
ep.x = px;
ep.y = py;
Path.find_intersection_handle (ep.get_right_handle (), selected_point.get_right_handle (), out cx, out cy);
}
public void press (int button, int x, int y, bool double_click) {
Glyph? g = MainWindow.get_current_glyph ();
Glyph glyph = (!) g;
bool reflective;
return_if_fail (g != null);
if ((double_click && !BirdFont.android)
|| Toolbox.drawing_tools.insert_point_on_path_tool.is_selected ()) {
glyph.insert_new_point_on_path (x, y);
return;
}
if (button == 1) {
add_point_event (x, y);
return;
}
if (button == 2) {
if (glyph.is_open ()) {
force_direction ();
glyph.close_path ();
} else {
glyph.open_path ();
}
return;
}
if (button == 3 && !KeyBindings.has_shift () ) {
glyph.clear_active_paths ();
selected_path = active_path;
move_point_event (x, y);
// alt+click on a handle ends the symmetrical editing
if ((KeyBindings.has_alt () || KeyBindings.has_ctrl ())
&& is_over_handle (x, y)) {
// don't use set point to reflective to on open ends
reflective = true;
foreach (Path path in MainWindow.get_current_glyph ().active_paths) {
if (path.is_open () && path.points.size > 0) {
if (selected_handle.parent == path.get_first_point ()
|| selected_handle.parent == path.get_last_point ()) {
reflective = false;
}
}
}
if (reflective) {
selected_handle.parent.set_reflective_handles (false);
selected_handle.parent.set_tie_handle (false);
GlyphCanvas.redraw ();
}
}
return;
}
if (button == 3) {
selected_path = active_path;
move_point_event (x, y);
}
if (KeyBindings.has_shift () && !is_over_handle (x, y)) {
show_selection_box = true;
return;
}
}
public void add_point_event (int x, int y) {
Glyph? g = MainWindow.get_current_glyph ();
Glyph glyph = (!) g;
PointSelection ps;
if (move_selected_handle) {
warning ("moving handle");
return;
}
return_if_fail (g != null);
remove_all_selected_points ();
ps = new_point_action (x, y);
active_path = ps.path;
glyph.store_undo_state ();
}
public void move_point_event (int x, int y) {
Glyph? g = MainWindow.get_current_glyph ();
Glyph glyph = (!) g;
return_if_fail (g != null);
control_point_event (x, y);
curve_corner_event (x, y);
if (!move_selected_handle) {
select_active_point (x, y);
last_selected_is_handle = false;
}
if (!KeyBindings.has_shift ()
&& selected_points.size == 0
&& !active_handle.active) {
show_selection_box = true;
}
glyph.store_undo_state ();
}
void set_type_for_moving_handle () {
if (selected_handle.type == PointType.LINE_CUBIC) {
selected_handle.set_point_type (PointType.CUBIC);
}
if (selected_handle.type == PointType.LINE_QUADRATIC) {
selected_handle.set_point_type (PointType.QUADRATIC);
}
if (selected_handle.type == PointType.LINE_DOUBLE_CURVE) {
selected_handle.set_point_type (PointType.DOUBLE_CURVE);
}
}
/** Set fill property to transparend for counter paths. */
public static void force_direction () {
Glyph g = MainWindow.get_current_glyph ();
clear_directions ();
foreach (Path p in g.get_visible_paths ()) {
if (!p.has_direction ()) {
if (is_counter_path (p)) {
p.force_direction (Direction.COUNTER_CLOCKWISE);
} else {
p.force_direction (Direction.CLOCKWISE);
}
}
}
update_selected_points ();
}
public static void clear_directions () {
clockwise.clear ();
counter_clockwise.clear ();
}
public static bool is_counter_path (Path path) {
Glyph g = MainWindow.get_current_glyph ();
PathList pl = new PathList ();
foreach (Path p in g.get_visible_paths ()) {
pl.add (p);
}
return Path.is_counter (pl, path);
}
public void remove_from_selected (EditPoint ep)
requires (selected_points.size > 0) {
Gee.ArrayList<PointSelection> remove = new Gee.ArrayList<PointSelection> ();
foreach (PointSelection e in selected_points) {
if (e.point.equals (e.point)) {
remove.add (e);
}
}
foreach (PointSelection e in remove) {
selected_points.remove (e);
}
}
public void select_active_point (double x, double y) {
Glyph glyph = MainWindow.get_current_glyph ();
bool reverse;
control_point_event (x, y);
// continue adding points from the other end of the selected path
reverse = false;
foreach (Path p in glyph.get_visible_paths ()) {
if (p.is_open () && p.points.size >= 1
&& (active_edit_point == p.points.get (0)
|| active_edit_point == p.points.get (p.points.size - 1))) {
update_selection ();
reverse = true;
control_point_event (x, y);
break;
}
}
foreach (Path p in glyph.get_visible_paths ()) {
if (p.is_open () && p.points.size > 1 && active_edit_point == p.points.get (0)) {
p.reverse ();
update_selection ();
reverse = true;
control_point_event (x, y);
break;
}
}
if (active_edit_point == null && KeyBindings.modifier != SHIFT) {
remove_all_selected_points ();
}
move_selected = true;
move_point_on_path = true;
if (active_edit_point != null) {
glyph.clear_active_paths ();
glyph.add_active_path (null, active_path);
DrawingTools.update_stroke_settings ();
if (KeyBindings.modifier != SHIFT) {
selected_point = (!)active_edit_point;
if (!((!)active_edit_point).is_selected ()) {
remove_all_selected_points ();
((!)active_edit_point).set_selected (true);
selected_point = (!)active_edit_point;
add_selected_point (selected_point, active_path); // FIXME: double check active path
last_selected_is_handle = false;
}
// alt+click creates a point with symmetrical handles
if (KeyBindings.has_alt () || KeyBindings.has_ctrl ()) {
selected_point.set_reflective_handles (true);
selected_point.get_right_handle ().process_symmetrical_handle ();
GlyphCanvas.redraw ();
}
}
}
if (reverse) {
clockwise.clear ();
counter_clockwise.clear ();
}
}
public static Path? find_path_to_join (EditPoint end_point) {
Path? m = null;
Glyph glyph = MainWindow.get_current_glyph ();
EditPoint ep_last, ep_first;
foreach (Path path in glyph.get_visible_paths ()) {
if (path.points.size == 0) {
continue;
}
ep_last = path.points.get (path.points.size - 1);
ep_first = path.points.get (0);
if (end_point == ep_last) {
m = path;
break;
}
if (end_point == ep_first) {
m = path;
break;
}
}
return m;
}
public static Path merge_open_paths (Path path1, Path path2) {
Path union, merge;
EditPoint first_point;
union = path1.copy ();
merge = path2.copy ();
return_val_if_fail (path1.points.size >= 1, merge);
return_val_if_fail (path2.points.size >= 1, union);
merge.points.get (0).set_tie_handle (false);
merge.points.get (0).set_reflective_handles (false);
merge.points.get (merge.points.size - 1).set_tie_handle (false);
merge.points.get (merge.points.size - 1).set_reflective_handles (false);
union.points.get (union.points.size - 1).set_tie_handle (false);
union.points.get (union.points.size - 1).set_reflective_handles (false);
union.points.get (0).set_tie_handle (false);
union.points.get (0).set_reflective_handles (false);
first_point = merge.get_first_point ();
if (union.get_last_point ().get_left_handle ().is_curve ()) {
first_point.get_left_handle ().convert_to_curve ();
} else {
if (first_point.type != PointType.QUADRATIC) {
first_point.get_left_handle ().convert_to_line ();
}
}
if (first_point.type != PointType.QUADRATIC) {
first_point.get_left_handle ().move_to_coordinate_internal (
union.get_last_point ().get_left_handle ().x,
union.get_last_point ().get_left_handle ().y);
}
union.delete_last_point ();
union.append_path (merge);
return union;
}
public static void close_path (Path path) {
bool last_segment_is_line;
bool first_segment_is_line;
return_if_fail (path.points.size > 1);
last_segment_is_line = path.get_last_point ().get_left_handle ().is_line ();
first_segment_is_line = path.get_first_point ().get_right_handle ().is_line ();
// TODO: set point type
path.points.get (0).left_handle.move_to_coordinate (
path.points.get (path.points.size - 1).left_handle.x,
path.points.get (path.points.size - 1).left_handle.y);
path.points.get (0).left_handle.type =
path.points.get (path.points.size - 1).left_handle.type;
path.recalculate_linear_handles_for_point (path.points.get (0));
path.recalculate_linear_handles_for_point (path.points.get (path.points.size - 1));
// force the connected handle to move
path.points.get (0).set_position (
path.points.get (0).x, path.points.get (0).y);
path.points.remove_at (path.points.size - 1);
path.close ();
if (last_segment_is_line) {
path.get_first_point ().get_left_handle ().convert_to_line ();
path.recalculate_linear_handles_for_point (path.get_first_point ());
}
if (first_segment_is_line) {
path.get_first_point ().get_right_handle ().convert_to_line ();
path.recalculate_linear_handles_for_point (path.get_first_point ());
}
}
/** @return the new path or null if no path could be merged with the end point. */
public static Path? join_paths (EditPoint end_point) {
Glyph glyph = MainWindow.get_current_glyph ();
Path? p;
Path path;
Path union;
bool direction_changed = false;
int px, py;
var paths = glyph.get_visible_paths ();
if (paths.size == 0) {
warning ("No paths.");
return null;
}
p = find_path_to_join (end_point);
if (p == null) {
warning ("No path to join.");
return null;
}
path = (!) p;
if (!path.is_open ()) {
warning ("Path is closed.");
return null;
}
return_val_if_fail (path.points.size > 0, null);
px = Glyph.reverse_path_coordinate_x (end_point.x);
py = Glyph.reverse_path_coordinate_y (end_point.y);
if (path.points.size == 1) {
glyph.delete_path (path);
glyph.clear_active_paths ();
foreach (Path merge in paths) {
if (merge.points.size > 0) {
if (is_close_to_point (merge.points.get (merge.points.size - 1), px, py)) {
glyph.add_active_path (null, merge);
active_path = merge;
merge.reopen ();
glyph.open_path ();
return merge;
}
if (is_close_to_point (merge.points.get (0), px, py)) {
glyph.add_active_path (null, merge);
active_path = merge;
clear_directions ();
merge.reopen ();
glyph.open_path ();
merge.reverse ();
return merge;
}
}
}
warning ("No point to merge.");
return null;
}
if (active_edit_point == path.points.get (0)) {
path.reverse ();
update_selection ();
path.recalculate_linear_handles ();
direction_changed = true;
active_edit_point = path.points.get (path.points.size - 1);
active_path = path;
}
if (path.points.get (0) == active_edit_point) {
warning ("Wrong direction.");
return null;
}
// join path with it self
if (is_close_to_point (path.points.get (0), px, py)) {
close_path (path);
glyph.close_path ();
force_direction ();
glyph.clear_active_paths ();
glyph.add_active_path (null, path);
path.close ();
if (direction_changed) {
path.reverse ();
update_selection ();
}
remove_all_selected_points ();
return path;
}
foreach (Path merge in paths) {
// don't join path with itself here
if (path == merge) {
continue;
}
// we need both start and end points
if (merge.points.size <= 1 || path.points.size <= 1) {
continue;
}
if (is_close_to_point (merge.points.get (merge.points.size - 1), px, py)) {
merge.reverse ();
update_selection ();
direction_changed = !direction_changed;
}
return_val_if_fail (merge.points.size > 0, null);
if (is_close_to_point (merge.points.get (0), px, py)) {
if (path.points.size == 1) {
warning ("path.points.size == 1\n");
} else if (merge.points.size == 1) {
warning ("merge.points.size == 1\n");
} else {
union = merge_open_paths (path, merge);
glyph.add_path (union);
glyph.delete_path (path);
glyph.delete_path (merge);
glyph.clear_active_paths ();
glyph.add_active_path (null, union);
union.reopen ();
union.create_list ();
force_direction ();
if (direction_changed) {
path.reverse ();
update_selection ();
}
union.update_region_boundaries ();
return union;
}
}
}
if (direction_changed) {
path.reverse ();
update_selection ();
}
warning ("No paths merged.");
return null;
}
/** Merge paths if ends are close. */
public static bool is_close_to_point (EditPoint ep, double x, double y) {
double px, py, distance;
px = Glyph.reverse_path_coordinate_x (ep.x);
py = Glyph.reverse_path_coordinate_y (ep.y);
distance = sqrt (fabs (pow (px - x, 2)) + fabs (pow (py - y, 2)));
return (distance < 7);
}
/** Show the user that curves will be merged on release. */
public void draw_on_canvas (Context cr, Glyph glyph) {
if (show_selection_box) {
draw_selection_box (cr);
}
if (point_selection_image) {
draw_point_selection_circle (cr);
}
draw_merge_icon (cr);
}
/** Higlight the selected point on Android. */
void draw_point_selection_circle (Context cr) {
PointSelection ps;
if (active_handle.active) {
Path.draw_control_point (cr, Glyph.path_coordinate_x (begin_action_x),
Glyph.path_coordinate_y (begin_action_y), Theme.get_color ("Active Handle"));
} else if (selected_points.size > 0) {
ps = selected_points.get (selected_points.size - 1);
if (ps.point.type == PointType.CUBIC) {
Path.draw_control_point (cr, Glyph.path_coordinate_x (begin_action_x),
Glyph.path_coordinate_y (begin_action_y), Theme.get_color ("Selected Cubic Control Point"));
} else {
Path.draw_control_point (cr, Glyph.path_coordinate_x (begin_action_x),
Glyph.path_coordinate_y (begin_action_y), Theme.get_color ("Selected Quadratic Control Point"));
}
}
}
void draw_selection_box (Context cr) {
double x, y, w, h;
x = fmin (selection_box_x, selection_box_last_x);
y = fmin (selection_box_y, selection_box_last_y);
w = fmax (selection_box_x, selection_box_last_x) - x;
h = fmax (selection_box_y, selection_box_last_y) - y;
cr.save ();
Theme.color (cr, "Foreground 1");
cr.set_line_width (2);
cr.rectangle (x, y, w, h);
cr.stroke ();
cr.restore ();
}
public static void draw_join_icon (Context cr, double x, double y) {
cr.save ();
Theme.color (cr, "Merge");
cr.move_to (x, y);
cr.arc (x, y, 15, 0, 2 * Math.PI);
cr.close_path ();
cr.fill ();
cr.restore ();
}
void draw_merge_icon (Context cr) {
double x, y;
if (active_edit_point != null) {
get_tie_position ((!) active_edit_point, out x, out y);
draw_join_icon (cr, x, y);
}
}
/** Obtain the position where to ends meet. */
static void get_tie_position (EditPoint current_point, out double x, out double y) {
Glyph glyph;
EditPoint active;
double px, py;
x = -100;
y = -100;
if (!is_endpoint (current_point)) {
return;
}
glyph = MainWindow.get_current_glyph ();
active = current_point;
return_if_fail (!is_null (glyph));
px = Glyph.reverse_path_coordinate_x (active.x);
py = Glyph.reverse_path_coordinate_y (active.y);
foreach (Path path in glyph.get_visible_paths ()) {
if (!path.is_open ()) {
continue;
}
if (path.points.size == 0) {
continue;
}
foreach (EditPoint ep in path.points) {
if (ep == active || !is_endpoint (ep)) {
continue;
}
if (is_close_to_point (ep, px, py)) {
x = Glyph.reverse_path_coordinate_x (ep.x);
y = Glyph.reverse_path_coordinate_y (ep.y);
return;
}
}
}
}
public static bool is_endpoint (EditPoint ep) {
EditPoint start;
EditPoint end;
Glyph glyph = MainWindow.get_current_glyph ();
foreach (Path path in glyph.get_visible_paths ()) {
if (path.points.size < 1) {
continue;
}
start = path.points.get (0);
end = path.points.get (path.points.size - 1);
if (ep == start || ep == end) {
return true;
}
}
return false;
}
public static void set_active_edit_point (EditPoint? e, Path path) {
bool redraw;
Glyph g = MainWindow.get_current_glyph ();
foreach (Path p in g.get_visible_paths ()) {
foreach (var ep in p.points) {
ep.set_active (false);
}
}
redraw = active_edit_point != e;
active_edit_point = e;
active_path = path;
if (e != null) {
((!)e).set_active (true);
}
if (redraw) {
GlyphCanvas.redraw ();
}
}
PointSelection? get_closest_point (double ex, double ey, out Path? path) {
double x = Glyph.path_coordinate_x (ex);
double y = Glyph.path_coordinate_y (ey);
double d = double.MAX;
double nd;
PointSelection? ep = null;
Glyph g = MainWindow.get_current_glyph ();
path = null;
foreach (Path current_path in g.get_paths_in_current_layer ()) {
if (is_close_to_path (current_path, ex, ey)) {
foreach (EditPoint e in current_path.points) {
nd = e.get_distance (x, y);
if (nd < d) {
d = nd;
ep = new PointSelection (e, current_path);
path = current_path;
}
}
}
}
return ep;
}
public double get_distance_to_closest_edit_point (double event_x, double event_y) {
Path? p;
PointSelection e;
PointSelection? ep = get_closest_point (event_x, event_y, out p);
double x = Glyph.path_coordinate_x (event_x);
double y = Glyph.path_coordinate_y (event_y);
if (ep == null) {
return double.MAX;
}
e = (!) ep;
return e.point.get_distance (x, y);
}
public void control_point_event (double event_x, double event_y, bool reset_active_point = true) {
Path? p;
PointSelection? ep = get_closest_point (event_x, event_y, out p);
Glyph g = MainWindow.get_current_glyph ();
double x = Glyph.path_coordinate_x (event_x);
double y = Glyph.path_coordinate_y (event_y);
double distance;
PointSelection e;
if (reset_active_point) {
set_active_edit_point (null, new Path ());
}
if (ep == null) {
return;
}
e = (!) ep;
distance = e.point.get_distance (x, y) * g.view_zoom;
if (distance < contact_surface) {
set_active_edit_point (e.point, e.path);
}
}
public PointSelection new_point_action (int x, int y) {
Glyph glyph;
PointSelection new_point;
glyph = MainWindow.get_current_glyph ();
glyph.open_path ();
remove_all_selected_points ();
new_point = add_new_edit_point (x, y);
new_point.point.set_selected (true);
selected_point = new_point.point;
active_edit_point = new_point.point;
return_val_if_fail (glyph.active_paths.size > 0, new PointSelection.empty ());
add_selected_point (selected_point, glyph.active_paths.get (glyph.active_paths.size - 1));
active_path = new_point.path;
glyph.clear_active_paths ();
glyph.add_active_path (null, new_point.path);
move_selected = true;
return new_point;
}
public static PointSelection add_new_edit_point (int x, int y) {
PointSelection new_point;
new_point = insert_edit_point (x, y);
new_point.path.update_region_boundaries ();
if (new_point.path.is_open () && new_point.path.points.size > 0) {
new_point.path.get_first_point ().set_reflective_handles (false);
new_point.path.get_first_point ().set_tie_handle (false);
new_point.path.get_last_point ().set_reflective_handles (false);
new_point.path.get_last_point ().set_tie_handle (false);
}
selected_point = new_point.point;
active_edit_point = new_point.point;
set_point_type (selected_point);
set_default_handle_positions ();
selected_points.clear ();
add_selected_point (new_point.point, new_point.path);
return new_point;
}
private static PointSelection insert_edit_point (double x, double y) {
double xt, yt;
Path np;
EditPoint inserted;
bool stroke = StrokeTool.add_stroke;
Glyph g = MainWindow.get_current_glyph ();
if (g.active_paths.size == 0) {
np = new Path ();
g.add_path (np);
np.stroke = stroke ? StrokeTool.stroke_width : 0;
np.line_cap = StrokeTool.line_cap;
g.add_active_path (null, np);
active_path = np;
selected_path = np;
}
xt = Glyph.path_coordinate_x (x);
yt = Glyph.path_coordinate_y (y);
if (selected_path.is_open ()) {
np = PenTool.selected_path;
np.add (xt, yt);
} else {
np = new Path ();
np.stroke = stroke ? StrokeTool.stroke_width : 0;
g.add_path (np);
np.add (xt, yt);
if (DrawingTools.pen_tool.is_selected ()) {
np.stroke = PenTool.path_stroke_width;
}
PenTool.active_path = np;
}
g.clear_active_paths ();
g.add_active_path (null, np);
active_path = np;
selected_path = np;
inserted = np.points.get (np.points.size - 1);
return new PointSelection (inserted, np);
}
static void set_point_type (EditPoint p) {
if (p.prev != null && p.get_prev ().right_handle.type == PointType.QUADRATIC) {
p.left_handle.type = PointType.QUADRATIC;
p.right_handle.type = PointType.LINE_QUADRATIC;
p.type = PointType.QUADRATIC;
} else if (DrawingTools.get_selected_point_type () == PointType.QUADRATIC) {
p.left_handle.type = PointType.LINE_QUADRATIC;
p.right_handle.type = PointType.LINE_QUADRATIC;
p.type = PointType.LINE_QUADRATIC;
} else if (DrawingTools.get_selected_point_type () == PointType.DOUBLE_CURVE) {
p.left_handle.type = PointType.LINE_DOUBLE_CURVE;
p.right_handle.type = PointType.LINE_DOUBLE_CURVE;
p.type = PointType.DOUBLE_CURVE;
} else {
p.left_handle.type = PointType.LINE_CUBIC;
p.right_handle.type = PointType.LINE_CUBIC;
p.type = PointType.CUBIC;
}
}
public static void set_default_handle_positions () {
Glyph g = MainWindow.get_current_glyph ();
foreach (var p in g.get_visible_paths ()) {
if (p.is_editable ()) {
p.create_list ();
set_default_handle_positions_on_path (p);
}
}
}
static void set_default_handle_positions_on_path (Path path) {
foreach (EditPoint e in path.points) {
if (!e.tie_handles && !e.reflective_point) {
path.recalculate_linear_handles_for_point (e);
}
}
}
private bool is_over_handle (double event_x, double event_y) {
Glyph g = MainWindow.get_current_glyph ();
double distance_to_edit_point = g.view_zoom * get_distance_to_closest_edit_point (event_x, event_y);
if (!Path.show_all_line_handles) {
foreach (PointSelection selected_corner in selected_points) {
if (is_close_to_handle (selected_corner.point, event_x, event_y, distance_to_edit_point)) {
return true;
}
}
} else {
foreach (Path p in g.get_visible_paths ()) {
foreach (EditPoint ep in p.points) {
if (is_close_to_handle (ep, event_x, event_y, distance_to_edit_point)) {
return true;
}
}
}
}
return false;
}
bool is_close_to_path (Path p, double event_x, double event_y) {
double c = contact_surface * Glyph.ivz ();
double x = Glyph.path_coordinate_x (event_x);
double y = Glyph.path_coordinate_y (event_y);
if (unlikely (!p.has_region_boundaries ())) {
if (p.points.size > 0) {
warning (@"No bounding box. $(p.points.size)");
p.update_region_boundaries ();
}
}
return p.xmin - c - 10 <= x <= p.xmax + c + 10 && p.ymin - c - 10 <= y <= p.ymax + c + 10 ;
}
private bool is_close_to_handle (EditPoint selected_corner, double event_x, double event_y, double distance_to_edit_point) {
double x = Glyph.path_coordinate_x (event_x);
double y = Glyph.path_coordinate_y (event_y);
Glyph g = MainWindow.get_current_glyph ();
double d_point = distance_to_edit_point;
double dl, dr;
dl = g.view_zoom * selected_corner.get_left_handle ().get_point ().get_distance (x, y);
dr = g.view_zoom * selected_corner.get_right_handle ().get_point ().get_distance (x, y);
if (dl < contact_surface && dl < d_point) {
return true;
}
if (dr < contact_surface && dr < d_point) {
return true;
}
return false;
}
PointSelection get_closest_handle (double event_x, double event_y) {
EditPointHandle left, right;
double x = Glyph.path_coordinate_x (event_x);
double y = Glyph.path_coordinate_y (event_y);
EditPointHandle eh = new EditPointHandle.empty();
Glyph g = MainWindow.get_current_glyph ();
double d = double.MAX;
double dn;
Path path = new Path ();
bool left_handle = false;
EditPoint parent_point;
EditPoint tied_point;
foreach (Path p in g.get_paths_in_current_layer ()) {
foreach (EditPoint ep in p.points) {
if (ep.is_selected () || Path.show_all_line_handles) {
left = ep.get_left_handle ();
right = ep.get_right_handle ();
dn = left.get_point ().get_distance (x, y);
if (dn < d) {
eh = left;
d = dn;
path = p;
left_handle = true;
}
dn = right.get_point ().get_distance (x, y);
if (dn < d) {
eh = right;
d = dn;
path = p;
left_handle = false;
}
}
}
}
// Make sure the selected handle belongs to the selected point if
// the current segment is quadratic.
if (eh.type == PointType.QUADRATIC) {
parent_point = eh.get_parent ();
if (left_handle) {
if (parent_point.prev != null) {
tied_point = parent_point.get_prev ();
if (tied_point.selected_point) {
eh = tied_point.get_right_handle ();
}
}
} else {
if (parent_point.next != null) {
tied_point = parent_point.get_next ();
if (tied_point.selected_point) {
eh = tied_point.get_left_handle ();
}
}
}
}
return new PointSelection.handle_selection (eh, path);
}
private void curve_active_corner_event (double event_x, double event_y) {
PointSelection eh;
Glyph glyph;
glyph = MainWindow.get_current_glyph ();
active_handle.active = false;
if (!is_over_handle (event_x, event_y)) {
return;
}
eh = get_closest_handle (event_x, event_y);
eh.handle.active = true;
if (active_handle != eh.handle) {
GlyphCanvas.redraw ();
}
active_handle = eh.handle;
}
private void curve_corner_event (double event_x, double event_y) {
Glyph g = MainWindow.get_current_glyph ();
g.open_path ();
PointSelection p;
if (!is_over_handle (event_x, event_y)) {
return;
}
move_selected_handle = true;
last_selected_is_handle = true;
selected_handle.selected = false;
p = get_closest_handle (event_x, event_y);
selected_handle = p.handle;
handle_selection = p;
selected_handle.selected = true;
active_path = p.path;
g.add_active_path (null, active_path);
}
public static void add_selected_point (EditPoint p, Path path) {
bool in_path = false;
foreach (EditPoint e in path.points) {
if (e == p) {
in_path = true;
break;
}
}
if (!in_path) {
warning ("Point is not in path.");
}
foreach (PointSelection ep in selected_points) {
if (p == ep.point) {
return;
}
}
selected_points.add (new PointSelection (p, path));
}
public static void remove_all_selected_points () {
Glyph g = MainWindow.get_current_glyph ();
foreach (PointSelection ep in selected_points) {
ep.point.set_active (false);
ep.point.set_selected (false);
}
selected_points.clear ();
foreach (Path p in g.get_visible_paths ()) {
foreach (EditPoint e in p.points) {
e.set_active (false);
e.set_selected (false);
}
}
}
static void move_select_next_point (uint keyval) {
PointSelection next = new PointSelection.empty ();
if (selected_points.size == 0) {
return;
}
cancel_stroke_creator ();
switch (keyval) {
case Key.UP:
next = get_next_point_up ();
break;
case Key.DOWN:
next = get_next_point_down ();
break;
case Key.LEFT:
next = get_next_point_left ();
break;
case Key.RIGHT:
next = get_next_point_right ();
break;
default:
break;
}
set_selected_point (next.point, next.path);
GlyphCanvas.redraw ();
}
private static PointSelection get_next_point (double angle)
requires (selected_points.size != 0) {
PointSelection e = selected_points.get (selected_points.size - 1);
double right_angle = e.point.right_handle.angle;
double left_angle = e.point.left_handle.angle;
double min_right, min_left;
double min;
return_val_if_fail (e.point.next != null, new EditPoint ());
return_val_if_fail (e.point.prev != null, new EditPoint ());
// angle might be greater than 2 PI or less than 0
min_right = double.MAX;
min_left = double.MAX;
for (double i = -2 * PI; i <= 2 * PI; i += 2 * PI) {
min = fabs (right_angle - (angle + i));
if (min < min_right) {
min_right = min;
}
min = fabs (left_angle - (angle + i));
if (min < min_left) {
min_left = min;
}
}
if (min_right < min_left) {
return new PointSelection (e.point.get_next (), e.path);
}
return new PointSelection (e.point.get_prev (), e.path);
}
private static PointSelection get_next_point_up () {
return get_next_point (PI / 2);
}
private static PointSelection get_next_point_down () {
return get_next_point (PI + PI / 2);
}
private static PointSelection get_next_point_left () {
return get_next_point (PI);
}
private static PointSelection get_next_point_right () {
return get_next_point (0);
}
private static void set_selected_point (EditPoint ep, Path p) {
remove_all_selected_points ();
add_selected_point (ep, p);
set_active_edit_point (ep, p);
edit_active_corner = true;
ep.set_selected (true);
set_default_handle_positions ();
}
public static void select_point_up () {
move_select_next_point (Key.UP);
}
public static void select_point_down () {
move_select_next_point (Key.DOWN);
}
public static void select_point_right () {
move_select_next_point (Key.RIGHT);
}
public static void select_point_left () {
move_select_next_point (Key.LEFT);
}
/**
* Move the selected editpoint one pixel with keyboard irrespectivly of
* current zoom.
*/
void move_selected_points (uint keyval) {
Path? last_path = null;
cancel_stroke_creator ();
update_selected_points ();
if (!last_selected_is_handle) {
if (keyval == Key.UP) {
foreach (PointSelection e in selected_points) {
e.point.set_position (e.point.x, e.point.y + Glyph.ivz ());
e.path.recalculate_linear_handles_for_point (e.point);
}
}
if (keyval == Key.DOWN) {
foreach (PointSelection e in selected_points) {
e.point.set_position (e.point.x, e.point.y - Glyph.ivz ());
e.path.recalculate_linear_handles_for_point (e.point);
}
}
if (keyval == Key.LEFT) {
foreach (PointSelection e in selected_points) {
e.point.set_position (e.point.x - Glyph.ivz (), e.point.y);
e.path.recalculate_linear_handles_for_point (e.point);
}
}
if (keyval == Key.RIGHT) {
foreach (PointSelection e in selected_points) {
e.point.set_position (e.point.x + Glyph.ivz (), e.point.y);
e.path.recalculate_linear_handles_for_point (e.point);
}
}
last_path = null;
foreach (PointSelection e in selected_points) {
if (e.path != last_path) {
e.path.update_region_boundaries ();
last_path = e.path;
}
}
} else {
set_type_for_moving_handle ();
active_handle.active = false;
active_handle = new EditPointHandle.empty ();
if (keyval == Key.UP) {
selected_handle.move_delta_coordinate (0, 1 * Glyph.ivz ());
}
if (keyval == Key.DOWN) {
selected_handle.move_delta_coordinate (0, -1 * Glyph.ivz ());
}
if (keyval == Key.LEFT) {
selected_handle.move_delta_coordinate (-1 * Glyph.ivz (), 0);
}
if (keyval == Key.RIGHT) {
selected_handle.move_delta_coordinate (1 * Glyph.ivz (), 0);
}
}
reset_stroke ();
// TODO: redraw only the relevant parts
GlyphCanvas.redraw ();
}
public static void convert_point_to_line (EditPoint ep, bool both) {
ep.set_tie_handle (false);
ep.set_reflective_handles (false);
if (ep.next == null) {
// FIXME: write a new method for this case
// warning ("Next is null.");
}
if (ep.prev == null) {
warning ("Prev is null.");
}
if (ep.type == PointType.CUBIC || ep.type == PointType.LINE_CUBIC) {
ep.type = PointType.LINE_CUBIC;
if (both) {
ep.get_left_handle ().type = PointType.LINE_CUBIC;
ep.get_right_handle ().type = PointType.LINE_CUBIC;
}
if (ep.next != null && ep.get_next ().is_selected ()) {
ep.get_right_handle ().type = PointType.LINE_CUBIC;
}
if (ep.prev != null && ep.get_prev ().is_selected ()) {
ep.get_left_handle ().type = PointType.LINE_CUBIC;
}
}
if (ep.type == PointType.DOUBLE_CURVE| ep.type == PointType.LINE_DOUBLE_CURVE) {
ep.type = PointType.LINE_DOUBLE_CURVE;
if (both) {
ep.get_left_handle ().type = PointType.LINE_DOUBLE_CURVE;
ep.get_right_handle ().type = PointType.LINE_DOUBLE_CURVE;
}
if (ep.next != null && ep.get_next ().is_selected ()) {
ep.get_right_handle ().type = PointType.LINE_DOUBLE_CURVE;
}
if (ep.prev != null && ep.get_prev ().is_selected ()) {
ep.get_left_handle ().type = PointType.LINE_DOUBLE_CURVE;
}
}
if (ep.type == PointType.QUADRATIC || ep.type == PointType.LINE_QUADRATIC) {
ep.type = PointType.LINE_QUADRATIC;
if (both) {
ep.get_left_handle ().type = PointType.LINE_QUADRATIC;
ep.get_right_handle ().type = PointType.LINE_QUADRATIC;
if (ep.next != null) {
ep.get_next ().get_left_handle ().type = PointType.LINE_QUADRATIC;
}
if (ep.prev != null) {
ep.get_prev ().get_right_handle ().type = PointType.LINE_QUADRATIC;
}
}
if (ep.next != null && ep.get_next ().is_selected ()) {
ep.get_right_handle ().type = PointType.LINE_QUADRATIC;
ep.get_next ().get_left_handle ().type = PointType.LINE_QUADRATIC;
}
if (ep.prev != null && ep.get_prev ().is_selected ()) {
ep.get_left_handle ().type = PointType.LINE_QUADRATIC;
ep.get_prev ().get_right_handle ().type = PointType.LINE_QUADRATIC;
}
}
//FIXME: ep.recalculate_linear_handles ();
}
public static void convert_segment_to_line () {
if (selected_points.size == 0) {
return;
}
if (selected_points.size == 1) {
convert_point_to_line (selected_points.get (0).point, true);
} else {
foreach (PointSelection p in selected_points) {
convert_point_to_line (p.point, false);
}
}
foreach (PointSelection p in selected_points) {
p.path.recalculate_linear_handles_for_point (p.point);
}
}
public static bool is_line (PointType t) {
return t == PointType.LINE_QUADRATIC
|| t == PointType.LINE_DOUBLE_CURVE
|| t == PointType.LINE_CUBIC;
}
public static PointType to_line (PointType t) {
switch (t) {
case PointType.QUADRATIC:
return PointType.LINE_QUADRATIC;
case PointType.DOUBLE_CURVE:
return PointType.LINE_DOUBLE_CURVE;
case PointType.CUBIC:
return PointType.LINE_CUBIC;
default:
break;
}
return t;
}
public static PointType to_curve (PointType t) {
switch (t) {
case PointType.LINE_QUADRATIC:
return PointType.QUADRATIC;
case PointType.LINE_DOUBLE_CURVE:
return PointType.DOUBLE_CURVE;
case PointType.LINE_CUBIC:
return PointType.CUBIC;
default:
break;
}
if (unlikely (t == PointType.NONE)) {
warning ("Type is not set.");
}
return t;
}
public static void set_converted_handle_length (EditPointHandle e, PointType pt) {
if (e.type == PointType.QUADRATIC && pt == PointType.DOUBLE_CURVE) {
e.length *= 2;
e.length /= 4;
}
if (e.type == PointType.QUADRATIC && pt == PointType.CUBIC) {
e.length *= 2;
e.length /= 3;
}
if (e.type == PointType.DOUBLE_CURVE && pt == PointType.QUADRATIC) {
e.length *= 4;
e.length /= 2;
}
if (e.type == PointType.DOUBLE_CURVE && pt == PointType.CUBIC) {
e.length *= 4;
e.length /= 3;
}
if (e.type == PointType.CUBIC && pt == PointType.QUADRATIC) {
e.length *= 3;
e.length /= 2;
}
if (e.type == PointType.CUBIC && pt == PointType.DOUBLE_CURVE) {
e.length *= 3;
e.length /= 4;
}
}
public static void convert_point_segment_type (EditPoint first, EditPoint next, PointType point_type) {
bool line;
set_converted_handle_length (first.get_right_handle (), point_type);
set_converted_handle_length (next.get_left_handle (), point_type);
line = is_line (first.type)
&& is_line (first.get_right_handle ().type)
&& is_line (next.get_left_handle ().type);
if (!line) {
first.type = point_type;
} else {
first.type = to_line (point_type);
}
if (!line) {
first.get_right_handle ().type = point_type;
} else {
first.get_right_handle ().type = to_line (point_type);
}
if (!line) {
next.get_left_handle ().type = point_type;
} else {
next.get_left_handle ().type = to_line (point_type);
}
// process connected handle
if (point_type == PointType.QUADRATIC) {
first.set_position (first.x, first.y);
// FIXME: linear handles
}
}
public static void convert_point_type (EditPoint first, PointType point_type) {
convert_point_segment_type (first, first.get_next (), point_type);
}
public static void convert_point_types () {
Glyph glyph = MainWindow.get_current_glyph ();
glyph.store_undo_state ();
PointSelection selected = new PointSelection.empty ();
bool reset_selected = false;
EditPoint e;
if (selected_points.size == 1) {
selected = selected_points.get (0);
if (selected.point.next != null) {
selected_points.add (new PointSelection (selected.point.get_next (), selected.path));
selected.point.get_next ().set_selected (true);
}
if (selected.point.prev != null) {
selected_points.add (new PointSelection (selected.point.get_prev (), selected.path));
selected.point.get_next ().set_selected (true);
}
reset_selected = true;
}
foreach (PointSelection ps in selected_points) {
e = ps.point;
// convert segments not control points
if (e.next == null || !e.get_next ().is_selected ()) {
continue;
}
convert_point_type (e, DrawingTools.point_type);
ps.path.recalculate_linear_handles_for_point (ps.point);
}
if (reset_selected) {
remove_all_selected_points ();
selected_points.add (selected);
selected.point.set_selected (true);
}
foreach (Path p in glyph.get_visible_paths ()) {
p.update_region_boundaries ();
}
}
public static void update_selected_points () {
Glyph g = MainWindow.get_current_glyph ();
selected_points.clear ();
foreach (Path p in g.get_visible_paths ()) {
foreach (EditPoint ep in p.points) {
if (ep.is_selected ()) {
selected_points.add (new PointSelection (ep, p));
}
}
}
}
public void select_all_points () {
Glyph g = MainWindow.get_current_glyph ();
foreach (Path p in g.get_visible_paths ()) {
foreach (EditPoint ep in p.points) {
ep.set_selected (true);
add_selected_point (ep, p);
}
}
}
public static Path simplify (Path path, bool selected_segments = false, double threshold = 0.3) {
PointSelection ps;
EditPoint ep;
Path p1, new_path;
double d, sumd;
int i;
p1 = path.copy ();
new_path = p1.copy ();
i = 0;
sumd = 0;
while (i < new_path.points.size) {
ep = new_path.points.get (i);
ps = new PointSelection (ep, new_path);
d = PenTool.remove_point_simplify (ps);
sumd += d;
if (sumd < threshold) {
p1 = new_path.copy ();
} else {
new_path = p1.copy ();
sumd = 0;
i++;
}
}
new_path.update_region_boundaries ();
return new_path;
}
public void set_simplification_threshold (double t) {
simplification_threshold = t;
}
public static void move_handle_on_axis () {
on_axis = true;
}
}
}