diff --git a/src/Drawables/DrawablePath.vala b/src/Drawables/DrawablePath.vala
index 7c359a9ea..bad45e3ec 100644
--- a/src/Drawables/DrawablePath.vala
+++ b/src/Drawables/DrawablePath.vala
@@ -72,6 +72,23 @@ public class Akira.Drawables.DrawablePath : Drawable {
cr.curve_to (x0, y0, x2, y2, x1, y1);
cr.curve_to (x1, y1, x3, y3, x4, y4);
+ point_idx += 4;
+ } else if (commands[i] == Lib.Modes.PathEditMode.Type.BEZIER) {
+ var x1 = points[point_idx].x;
+ var y1 = points[point_idx].y;
+
+ var x2 = points[point_idx + 1].x;
+ var y2 = points[point_idx + 1].y;
+
+ var x3 = points[point_idx + 2].x;
+ var y3 = points[point_idx + 2].y;
+
+ var x4 = points[point_idx + 3].x;
+ var y4 = points[point_idx + 3].y;
+
+ cr.move_to (x1, y1);
+ cr.curve_to (x2, y2, x3, y3, x4, y4);
+
point_idx += 4;
}
}
diff --git a/src/Geometry/Point.vala b/src/Geometry/Point.vala
index 6d5766b64..6c8f861a4 100644
--- a/src/Geometry/Point.vala
+++ b/src/Geometry/Point.vala
@@ -31,6 +31,26 @@ public struct Akira.Geometry.Point {
this.y = y;
}
+ public Point add (Point pt) {
+ return Geometry.Point (x + pt.x, y + pt.y);
+ }
+
+ public Point sub (Point pt) {
+ return Geometry.Point (x - pt.x, y - pt.y);
+ }
+
+ public double dot (Point pt) {
+ return x * pt.x + y * pt.y;
+ }
+
+ public Point scale (double val) {
+ return Geometry.Point (x * val, y * val);
+ }
+
+ public double distance (Point pt) {
+ return Utils.GeometryMath.distance (x, y, pt.x, pt.y);
+ }
+
public Point.deserialized (Json.Object obj) {
x = obj.get_double_member ("x");
y = obj.get_double_member ("y");
diff --git a/src/Layouts/HeaderBar.vala b/src/Layouts/HeaderBar.vala
index f5eb89bff..300af47b7 100644
--- a/src/Layouts/HeaderBar.vala
+++ b/src/Layouts/HeaderBar.vala
@@ -293,7 +293,11 @@ public class Akira.Layouts.HeaderBar : Gtk.HeaderBar {
Akira.Services.ActionManager.ACTION_PREFIX +
Akira.Services.ActionManager.ACTION_PATH_TOOL);
- var pencil = create_model_button (_("Pencil"), "edit-symbolic", "P");
+ var pencil = create_model_button (
+ _("Pencil"),
+ "edit-symbolic",
+ Akira.Services.ActionManager.ACTION_PREFIX +
+ Akira.Services.ActionManager.ACTION_PENCIL_TOOL);
var text = create_model_button (
_("Text"),
diff --git a/src/Layouts/LayersList/LayerItemModel.vala b/src/Layouts/LayersList/LayerItemModel.vala
index 60b85dab2..90ef400c9 100644
--- a/src/Layouts/LayersList/LayerItemModel.vala
+++ b/src/Layouts/LayersList/LayerItemModel.vala
@@ -69,6 +69,8 @@ public class Akira.Layouts.LayersList.LayerItemModel : GLib.Object {
return "segment-curve-symbolic";
} else if (type is Lib.Items.ModelTypeGroup) {
return "folder-symbolic";
+ } else if (type is Lib.Items.ModelTypePencil) {
+ return "shape-pencil-symbolic";
}
return "";
}
diff --git a/src/Lib/Items/ModelTypePencil.vala b/src/Lib/Items/ModelTypePencil.vala
new file mode 100644
index 000000000..bbee17e6c
--- /dev/null
+++ b/src/Lib/Items/ModelTypePencil.vala
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2019-2021 Alecaddd (https://alecaddd.com)
+ *
+ * This file is part of Akira.
+ *
+ * Akira 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.
+
+ * Akira 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 Akira. If not, see .
+ *
+ * Authored by: Martin "mbfraga" Fraga
+ */
+
+public class Akira.Lib.Items.ModelTypePencil : ModelType {
+ public static ModelInstance minimal_rect () {
+ return default_path (
+ new Lib.Components.Coordinates (0.5, 0.5),
+ null,
+ null
+ );
+ }
+
+ public static ModelInstance default_path (
+ Lib.Components.Coordinates center,
+ Lib.Components.Borders? borders,
+ Lib.Components.Fills? fills
+ ) {
+ var new_item = new ModelInstance (-1, new ModelTypePencil ());
+ new_item.components.center = center;
+ new_item.components.borders = borders;
+ new_item.components.fills = fills;
+ new_item.components.transform = Lib.Components.Components.default_transform ();
+ new_item.components.flipped = Lib.Components.Components.default_flipped ();
+ new_item.components.border_radius = Lib.Components.Components.default_border_radius ();
+ new_item.components.path = new Lib.Components.Path.from_single_point (
+ Akira.Geometry.Point (center.x, center.y),
+ Lib.Modes.PathEditMode.Type.LINE,
+ false
+ );
+ new_item.components.size = new Lib.Components.Size (1, 1, false);
+ new_item.components.name = Lib.Components.Components.default_name ();
+ return new_item;
+ }
+
+ public override string name_id { get { return "pencil"; } }
+
+ public override Components.CompiledGeometry compile_geometry (
+ Components.Components? components,
+ Lib.Items.ModelNode? node
+ ) {
+ return new Components.CompiledGeometry.from_components (components, node, true);
+ }
+
+ public override void construct_canvas_item (ModelInstance instance) {
+ instance.drawable = new Drawables.DrawablePath (
+ (instance.components.path == null) ? null : instance.components.path.data
+ );
+ }
+
+ public override void component_updated (ModelInstance instance, Lib.Components.Component.Type type) {
+ switch (type) {
+ case Lib.Components.Component.Type.COMPILED_BORDER:
+ if (!instance.compiled_border.is_visible) {
+ instance.drawable.line_width = 0;
+ instance.drawable.stroke_rgba = Gdk.RGBA () { alpha = 0 };
+ break;
+ }
+
+ // The "line-width" property expects a DOUBLE type, but we don't support subpixels
+ // so we always handle the border size as INT, therefore we need to type cast it here.
+ instance.drawable.line_width = (double) instance.compiled_border.size;
+ instance.drawable.stroke_rgba = instance.compiled_border.color;
+ break;
+ case Lib.Components.Component.Type.COMPILED_FILL:
+ if (!instance.compiled_fill.is_visible) {
+ instance.drawable.fill_rgba = Gdk.RGBA () { alpha = 0 };
+ break;
+ }
+
+ instance.drawable.fill_rgba = instance.compiled_fill.color;
+ break;
+ case Lib.Components.Component.Type.COMPILED_GEOMETRY:
+ // The points property is only available to DrawablePath, so first typecast it
+ // modify it, then assign to instance.
+ Drawables.DrawablePath drawable = instance.drawable as Drawables.DrawablePath;
+ drawable.center_x = -instance.compiled_geometry.source_width / 2.0;
+ drawable.center_y = -instance.compiled_geometry.source_height / 2.0;
+ drawable.transform = instance.compiled_geometry.transformation_matrix;
+ drawable.points = instance.components.path.data;
+ drawable.commands = instance.components.path.commands;
+ break;
+ }
+ }
+}
diff --git a/src/Lib/Modes/AbstractInteractionMode.vala b/src/Lib/Modes/AbstractInteractionMode.vala
index 1c07b48fb..ae5aa8c17 100644
--- a/src/Lib/Modes/AbstractInteractionMode.vala
+++ b/src/Lib/Modes/AbstractInteractionMode.vala
@@ -51,7 +51,8 @@ public abstract class Akira.Lib.Modes.AbstractInteractionMode : Object {
ITEM_INSERT,
EXPORT,
PAN,
- PATH_EDIT
+ PATH_EDIT,
+ FREE_HAND
}
public signal void request_deregistration (ModeType type);
diff --git a/src/Lib/Modes/FreeHandMode.vala b/src/Lib/Modes/FreeHandMode.vala
new file mode 100644
index 000000000..7a6afe3bb
--- /dev/null
+++ b/src/Lib/Modes/FreeHandMode.vala
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2021 Alecaddd (https://alecaddd.com)
+ *
+ * This file is part of Akira.
+ *
+ * Akira 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.
+
+ * Akira 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 Akira. If not, see .
+ *
+ * Authored by: Ashish Shevale
+*/
+
+public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode {
+
+ public weak Lib.ViewCanvas view_canvas { get; construct; }
+ public Lib.Items.ModelInstance instance { get; construct; }
+ private Models.FreeHandModel edit_model;
+
+ private bool is_click = false;
+
+ public FreeHandMode (Lib.ViewCanvas canvas, Lib.Items.ModelInstance instance) {
+ Object (
+ view_canvas: canvas,
+ instance: instance
+ );
+
+ edit_model = new Models.FreeHandModel (instance, view_canvas);
+ }
+
+ public override AbstractInteractionMode.ModeType mode_type () {
+ return AbstractInteractionMode.ModeType.FREE_HAND;
+ }
+
+ public override Utils.Nobs.Nob active_nob () {
+ return Utils.Nobs.Nob.NONE;
+ }
+
+ public override void mode_begin () {
+ // Hide the nobs and show the path layer.
+ view_canvas.toggle_layer_visibility (ViewLayers.ViewLayer.NOBS_LAYER_ID, false);
+ view_canvas.toggle_layer_visibility (ViewLayers.ViewLayer.PATH_LAYER_ID, true);
+ }
+
+ public override void mode_end () {
+ // Hide the path layer and show nobs.
+ view_canvas.toggle_layer_visibility (ViewLayers.ViewLayer.NOBS_LAYER_ID, true);
+ view_canvas.toggle_layer_visibility (ViewLayers.ViewLayer.PATH_LAYER_ID, false);
+ }
+
+ public override Gdk.CursorType? cursor_type () {
+ return Gdk.CursorType.CROSSHAIR;
+ }
+
+ public override bool key_press_event (Gdk.EventKey event) {
+ return false;
+ }
+
+ public override bool key_release_event (Gdk.EventKey event) {
+ return false;
+ }
+
+ public override bool button_press_event (Gdk.EventButton event) {
+ is_click = true;
+
+ if (edit_model.first_point.x == -1) {
+ edit_model.first_point = Geometry.Point (event.x, event.y);
+ }
+ return true;
+ }
+
+ public override bool button_release_event (Gdk.EventButton event) {
+ is_click = false;
+ edit_model.fit_curve ();
+ view_canvas.mode_manager.deregister_active_mode ();
+ return true;
+ }
+
+ public override bool motion_notify_event (Gdk.EventMotion event) {
+ if (is_click) {
+ Geometry.Point point = Geometry.Point (event.x, event.y);
+ // print("add point %f %f\n", event.x, event.y);
+ edit_model.add_raw_point (point);
+ }
+ return true;
+ }
+
+ public override Object? extra_context () {
+ return null;
+ }
+}
diff --git a/src/Lib/Modes/ItemInsertMode.vala b/src/Lib/Modes/ItemInsertMode.vala
index ce6caecb2..44b4bb070 100644
--- a/src/Lib/Modes/ItemInsertMode.vala
+++ b/src/Lib/Modes/ItemInsertMode.vala
@@ -33,6 +33,7 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
private Lib.Modes.TransformMode transform_mode;
private Lib.Modes.PathEditMode path_edit_mode;
+ private Lib.Modes.FreeHandMode free_hand_mode;
public ItemInsertMode (Lib.ViewCanvas canvas, string item_type) {
Object (view_canvas: canvas);
@@ -42,6 +43,7 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
construct {
transform_mode = null;
path_edit_mode = null;
+ free_hand_mode = null;
}
public override AbstractInteractionMode.ModeType mode_type () {
@@ -56,6 +58,10 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
if (path_edit_mode != null) {
path_edit_mode.mode_end ();
}
+
+ if (free_hand_mode != null) {
+ free_hand_mode.mode_end ();
+ }
}
public override Gdk.CursorType? cursor_type () {
@@ -75,6 +81,10 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
return path_edit_mode.key_press_event (event);
}
+ if (free_hand_mode != null) {
+ return free_hand_mode.key_press_event (event);
+ }
+
return false;
}
@@ -94,6 +104,10 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
return path_edit_mode.button_press_event (event);
}
+ if (free_hand_mode != null) {
+ return free_hand_mode.button_press_event (event);
+ }
+
if (event.button == Gdk.BUTTON_PRIMARY) {
bool is_artboard;
var instance = construct_item (item_insert_type, event.x, event.y, out is_artboard);
@@ -113,6 +127,10 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
path_edit_mode = new Lib.Modes.PathEditMode (view_canvas, instance);
path_edit_mode.mode_begin ();
path_edit_mode.button_press_event (event);
+ } else if (item_insert_type == "pencil") {
+ free_hand_mode = new Lib.Modes.FreeHandMode (view_canvas, instance);
+ free_hand_mode.mode_begin ();
+ free_hand_mode.button_press_event (event);
} else {
transform_mode = new Lib.Modes.TransformMode (view_canvas, Utils.Nobs.Nob.BOTTOM_LEFT);
transform_mode.mode_begin ();
@@ -138,6 +156,10 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
return path_edit_mode.button_release_event (event);
}
+ if (free_hand_mode != null) {
+ return free_hand_mode.button_release_event (event);
+ }
+
return true;
}
@@ -150,6 +172,10 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
return path_edit_mode.motion_notify_event (event);
}
+ if (free_hand_mode != null) {
+ return free_hand_mode.motion_notify_event (event);
+ }
+
return true;
}
@@ -243,6 +269,25 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode {
var test_path = new Geometry.Point[1];
test_path[0] = Geometry.Point (0, 0);
+
+ Lib.Modes.PathEditMode.Type[] commands = { Lib.Modes.PathEditMode.Type.LINE };
+
+ new_item.components.path = new Lib.Components.Path.from_points (test_path, commands, false);
+ break;
+
+ case "pencil":
+ // A freehand curve is basically a path as we will be approximating
+ // the continuous set of points into the best fitting bezier curve.
+ new_item = Lib.Items.ModelTypePencil.default_path (
+ coordinates,
+ borders_from_settings (),
+ null
+ );
+
+ var test_path = new Geometry.Point[1];
+ test_path[0] = Geometry.Point (0, 0);
+
+
Lib.Modes.PathEditMode.Type[] commands = { Lib.Modes.PathEditMode.Type.LINE };
new_item.components.path = new Lib.Components.Path.from_points (test_path, commands, false);
diff --git a/src/Lib/Modes/PathEditMode.vala b/src/Lib/Modes/PathEditMode.vala
index 86b59b2b7..a76e0a098 100644
--- a/src/Lib/Modes/PathEditMode.vala
+++ b/src/Lib/Modes/PathEditMode.vala
@@ -22,7 +22,8 @@
public class Akira.Lib.Modes.PathEditMode : AbstractInteractionMode {
public enum Type {
LINE,
- CURVE
+ CURVE,
+ BEZIER
}
public weak Lib.ViewCanvas view_canvas { get; construct; }
diff --git a/src/Models/FreeHandModel.vala b/src/Models/FreeHandModel.vala
new file mode 100644
index 000000000..315d89615
--- /dev/null
+++ b/src/Models/FreeHandModel.vala
@@ -0,0 +1,545 @@
+/**
+ * Copyright (c) 2021 Alecaddd (https://alecaddd.com)
+ *
+ * This file is part of Akira.
+ *
+ * Akira 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.
+
+ * Akira 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 Akira. If not, see .
+ *
+ * Authored by: Ashish Shevale
+*/
+
+public class Akira.Models.FreeHandModel : Object {
+ public Lib.Items.ModelInstance instance { get; construct; }
+ public unowned Lib.ViewCanvas view_canvas { get; construct; }
+
+ private ViewLayers.ViewLayerPath path_layer;
+
+ // These store a copy of points and commands in the path.
+ private Lib.Modes.PathEditMode.Type[] commands;
+ private Geometry.Point[] points;
+
+ public Geometry.Point first_point;
+
+ // This stores the raw points taken directly from input events.
+ // These points are further processed to fit a curve.
+ private Geometry.Point[] raw_points;
+
+ // This is the error that we allow for approximating bezier curves.
+ private const double TOLERANCE = 10.0;
+
+ // This class provides methods for operating on bezier curves.
+ private class Bezier {
+ // Calculates the value of bezier curve at t. Returns that point.
+ public static Geometry.Point q (Geometry.Point[] ctrl, double t) {
+ var tx = 1.0 - t;
+ var pa = ctrl[0].scale (tx * tx * tx);
+ var pb = ctrl[1].scale (3 * tx * tx * t);
+ var pc = ctrl[2].scale (3 * tx * t * t);
+ var pd = ctrl[3].scale (t * t * t);
+
+ return pa.add (pb).add (pc).add (pd);
+ }
+
+ // Calculates value of first derivative of bezier curve at t. Returns that point.
+ public static Geometry.Point q_prime (Geometry.Point[] ctrl, double t) {
+ var tx = 1.0 - t;
+ var pa = ctrl[1].sub (ctrl[0]).scale (3 * tx * tx);
+ var pb = ctrl[2].sub (ctrl[1]).scale (3 * tx * tx);
+ var pc = ctrl[3].sub (ctrl[2]).scale (3 * tx * tx);
+
+ return pa.add (pb).add (pc);
+ }
+
+ // Calculates value of second derivative of bezier curve at t. Returns that point.
+ public static Geometry.Point q_prime_prime (Geometry.Point[] ctrl, double t) {
+ var tx = 1.0 - t;
+ var pa = ctrl[2].sub (ctrl[1].scale (2)).add (ctrl[0]).scale (6 * tx);
+ var pb = ctrl[3].sub (ctrl[2].scale (2)).add (ctrl[1]).scale (6 * tx);
+
+ return pa.add (pb);
+ }
+ }
+
+ public FreeHandModel (Lib.Items.ModelInstance instance, Lib.ViewCanvas view_canvas) {
+ Object (
+ view_canvas: view_canvas,
+ instance: instance
+ );
+
+ first_point = Geometry.Point (-1, -1);
+
+ commands = instance.components.path.commands;
+ points = instance.components.path.data;
+
+ raw_points = new Geometry.Point[0];
+
+ // Layer to show when editing paths.
+ path_layer = new ViewLayers.ViewLayerPath ();
+ path_layer.add_to_canvas (ViewLayers.ViewLayer.PATH_LAYER_ID, view_canvas);
+
+ update_view ();
+ }
+
+ public void add_raw_point (Geometry.Point point) {
+ raw_points.resize (raw_points.length + 1);
+ raw_points[raw_points.length - 1] = point.sub (first_point);
+
+ update_view ();
+ }
+
+ public void fit_curve () {
+ var len = raw_points.length;
+
+ Geometry.Point left_tan = normalize (raw_points[1].sub (raw_points[len]));
+ Geometry.Point right_tan = normalize (raw_points[len - 2].sub (raw_points[len - 1]));
+
+ points = fit_cubic (raw_points, 0, len - 1, left_tan, right_tan);
+
+ commands = new Lib.Modes.PathEditMode.Type[points.length / 4];
+ for (int i = 0; i < commands.length; ++i) {
+ commands[i] = Lib.Modes.PathEditMode.Type.BEZIER;
+ }
+
+ points = recalculate_points (points);
+ instance.components.path = new Lib.Components.Path.from_points (points, commands);
+ recompute_components ();
+ }
+
+ /*
+ * This method tries to fit a bezier curve onto the given set of points.
+ * If the error between the approximation is too large, then we divide the points into 2 parts
+ * apply the same algorithm recursively.
+
+ * pts => the list of all points.
+ * first => the starting index of points we will consider in this iteration.
+ * last => the end index of points we will consider in this iteration.
+ * left_tan => the left tangent vector. Direction is same as line joining first 2 points.
+ * right_tan => the right tangent vector. Direction is same as line joining last 2 points.
+ */
+ private Geometry.Point[] fit_cubic (
+ Geometry.Point[] pts,
+ int first, int last,
+ Geometry.Point left_tan,
+ Geometry.Point right_tan
+ ) {
+ // Number of times we will make adjustments in the points and try to refit before recursing.
+ var max_iterations = 4;
+ // The maximum error permissible before we try to divide and recurse.
+ var iteration_error = TOLERANCE * max_iterations;
+
+ // In case only 2 points are left, apply heuristics.
+ if ((last - first + 1) == 2) {
+ double dist = pts[last].distance (pts[first]) / 3.0;
+ Geometry.Point[] bez_curve = new Geometry.Point[4];
+ bez_curve[0] = pts[first];
+ bez_curve[1] = bez_curve[0].add (left_tan.scale (dist));
+ bez_curve[2] = bez_curve[3].add (right_tan.scale (dist));
+ bez_curve[3] = pts[last];
+
+ return bez_curve;
+ }
+
+ var u = chord_length_parameterize (pts, first, last);
+ var bez_curve = generate_bezier (pts, first, last, u, left_tan, right_tan);
+
+ int split;
+ var max_error = compute_max_error (pts, first, last, bez_curve, u, out split);
+
+ if (max_error < TOLERANCE) {
+ return bez_curve;
+ }
+
+ double[] u_prime;
+ // If the error exceeds the accepted limit, but is still within the max permissible bound,
+ // Reparameterize and try to fit again.
+ if (max_error < iteration_error) {
+
+ u_prime = u;
+ var prev_error = max_error;
+ var prev_split = split;
+
+ for (int i = 0; i < max_iterations; ++i) {
+ u_prime = reparameterize (pts, first, last, u_prime, bez_curve);
+ bez_curve = generate_bezier (pts, first, last, u_prime, left_tan, right_tan);
+ max_error = compute_max_error (pts, first, last, bez_curve, u, out split);
+
+ if (max_error < TOLERANCE) {
+ return bez_curve;
+ }
+
+ // If development of the fitted curve grinds to a halt,
+ // abort this attempt and try a shorter one.
+ if (split == prev_split) {
+ var err_change = (max_error / prev_error);
+ if (err_change > 0.9999 || err_change < 1.0001) {
+ break;
+ }
+ }
+
+ prev_error = max_error;
+ prev_split = split;
+ }
+ }
+
+ // Since the attempts till now failed, we will try to split our set of points into 2
+ // and try to fit a bezier curve on them separately.
+ // Inorder to maintain a smooth transition between these two segments,
+ // we use the points before and after the split point to create tangents.
+ var center_tan = pts[split - 1].sub (pts[split + 1]);
+ // If the point before and after the center are same, (e.g. self intersecting curve)
+ // use the point before and the center point for tangents.
+ if (center_tan.x == 0 && center_tan.y == 0) {
+ center_tan = pts[split - 1].sub (pts[split]);
+ center_tan.x = -1 * center_tan.y;
+ center_tan.y = center_tan.x;
+ }
+
+ var to_center_tangent = normalize (center_tan);
+ // The tangent for the second curve must point in the opposite direction.
+ var from_center_tangent = to_center_tangent.scale (-1);
+
+ // This array will store the result of fitting on both halves.
+ var cubic_curve = new Geometry.Point[0];
+ foreach (var it in fit_cubic (pts, first, split, left_tan, to_center_tangent)) {
+ cubic_curve += it;
+ }
+
+ foreach (var it in fit_cubic (pts, split, last, from_center_tangent, right_tan)) {
+ cubic_curve += it;
+ }
+
+ return cubic_curve;
+ }
+
+ // Assigns parameter values to points using relative distances.
+ double[] chord_length_parameterize (Geometry.Point[] pts, int first, int last) {
+ double[] u = new double[1];
+ u[0] = 0.0;
+
+ for (int i = first + 1; i <= last; ++i) {
+ u += u[i - first - 1] + pts[i].distance (pts[i - 1]);
+ }
+
+ for (int i = 0; i < u.length; ++i) {
+ u[i] = u[i] / u[u.length - 1];
+ }
+
+ return u;
+ }
+
+ // Approzimate a bezier curve on the given set of points.
+ // Check the book Graphics Gems I for mathematical details on how this is done.
+ // Basically, the first and last point of the points we are fitting will be
+ // the first and last point of the segment. Our sole job is to get the 2 tangents.
+ private Geometry.Point[] generate_bezier (
+ Geometry.Point[] pts,
+ int first,
+ int last,
+ double[] u_prime,
+ Geometry.Point left_tan,
+ Geometry.Point right_tan
+ ) {
+ var bez_curve = new Geometry.Point[4];
+ bez_curve[0] = pts[first];
+ bez_curve[3] = pts[last];
+ var n_pts = last - first + 1;
+
+ Geometry.Point[,] A = new Geometry.Point[n_pts, 2]; //vala-lint=naming-convention
+
+ for (int i = 0; i < n_pts; ++i) {
+ var u = u_prime[i];
+ var ux = 1.0 - u;
+ A[i, 0] = left_tan.scale (3 * u * ux * ux);
+ A[i, 1] = right_tan.scale (3 * u * u * ux);
+ }
+
+ double[,] C = new double[2, 2]; //vala-lint=naming-convention
+ double[] X = new double[2]; //vala-lint=naming-convention
+ C[0, 0] = C[0, 1] = C[1, 0] = C[1, 1] = 0;
+ X[0] = X[1] = 0;
+
+ for (int i = 0; i < u_prime.length; ++i) {
+ C[0, 0] += A[i, 0].dot (A[i, 0]);
+ C[0, 1] += A[i, 0].dot (A[i, 1]);
+ C[1, 0] = C[0, 1];
+ C[1, 1] += A[i, 1].dot (A[i, 1]);
+
+ var first_pt = pts[first];
+ var last_pt = pts[last];
+ Geometry.Point[] ctrl = {first_pt, first_pt, last_pt, last_pt};
+ var tmp = pts[i + first].sub (Bezier.q (ctrl, u_prime[i]));
+
+ X[0] += A[i, 0].dot (tmp);
+ X[1] += A[i, 1].dot (tmp);
+ }
+
+ // Calculate determinants of C and X.
+ var det_C0_C1 = C[0, 0] * C[1, 1] - C[1, 0] * C[0, 1]; //vala-lint=naming-convention
+ var det_C0_X = C[0, 0] * X[1] - C[1, 0] * X[0]; //vala-lint=naming-convention
+ var det_X_C1 = X[0] * C[1, 1] - X[1] * C[0, 1]; //vala-lint=naming-convention
+
+ double alpha_l = (det_C0_C1 == 0) ? 0 : (det_X_C1 / det_C0_C1);
+ double alpha_r = (det_C0_C1 == 0) ? 0 : (det_C0_X / det_C0_C1);
+
+ var seg_length = pts[last].distance (pts[first]);
+ var epsilon = 1.0e-6 * seg_length;
+
+ if (alpha_l < epsilon || alpha_r < epsilon) {
+ var dist = seg_length / 3.0;
+ //Fall back on standard (probably inaccurate) formula, and subdivide further if needed.
+ bez_curve[1] = bez_curve[0].add (left_tan.scale (dist));
+ bez_curve[2] = bez_curve[3].add (right_tan.scale (dist));
+
+ return bez_curve;
+ }
+
+ //First and last control points of the Bezier curve are
+ //positioned exactly at the first and last data points
+ //Control points 1 and 2 are positioned an alpha distance out
+ //on the tangent vectors, left and right, respectively
+ bez_curve[1] = bez_curve[0].add (left_tan.scale (alpha_l));
+ bez_curve[2] = bez_curve[3].add (right_tan.scale (alpha_r));
+
+ return bez_curve;
+ }
+
+ // If the bezier curve we get after fitting may not be the best.
+ // In these cases, we subdivide. But sometimes, we can readjust the
+ // parameterization to get better results. This is what we do here, using the Newton-Raphson Iteration.
+ private double[] reparameterize (
+ Geometry.Point[] pts,
+ int first,
+ int last,
+ double[] u,
+ Geometry.Point[] bez_curve
+ ) {
+
+ var u_prime = new double[u.length];
+ for (int i = first; i <= last; ++i) {
+ u_prime[i - first] = newton_raphson_root_find (bez_curve, pts[i], u[i - first]);
+ }
+
+ return u_prime;
+ }
+
+ private double newton_raphson_root_find (Geometry.Point[] bez_curve, Geometry.Point p, double u) {
+ var d = Bezier.q (bez_curve, u).sub (p);
+ var q_prime = Bezier.q_prime (bez_curve, u);
+
+ double numerator = d.dot (q_prime);
+ double denominator = norm (q_prime) * norm (q_prime) + 2 * Bezier.q_prime_prime (bez_curve, u).dot (d);
+
+ if (denominator == 0.0f) {
+ return u;
+ }
+
+ return (u - (numerator / denominator));
+ }
+
+ // Find the maximum square distance between the points and the bezier curve.
+ private double compute_max_error (
+ Geometry.Point[] pts,
+ int first,
+ int last,
+ Geometry.Point[] bez_curve,
+ double[] u,
+ out int split
+ ) {
+ split = (last + first) / 2;
+ double max_dist = 0.0;
+
+ var t_dist = map_to_relative_dist (bez_curve, 10);
+
+ for (int i = first; i <= last; ++i) {
+ var point = pts[i];
+ var t = find_t (bez_curve, u[i - first], t_dist, 10);
+ var v = Bezier.q (bez_curve, t).sub (point);
+
+ var dist = Math.pow (norm (v), 2);
+
+ if (dist > max_dist) {
+ max_dist = dist;
+ split = i;
+ }
+ }
+
+ return max_dist;
+ }
+
+ // Take samples of the bezier curve and map them to relative distances along the curve.
+ private double[] map_to_relative_dist (Geometry.Point[] bez_curve, double parts) {
+
+ var b_t_dist = new double[1];
+ b_t_dist[0] = 0;
+
+ var b_t_prev = bez_curve[0];
+ var sum_len = 0.0;
+
+ for (int i = 1; i <= parts; ++i) {
+ var b_t_curr = Bezier.q (bez_curve, i / parts);
+ sum_len += norm (b_t_curr.sub (b_t_prev));
+
+ b_t_dist += sum_len;
+ b_t_prev = b_t_curr;
+ }
+
+ for (int i = 0; i < b_t_dist.length; ++i) {
+ b_t_dist[i] /= sum_len;
+ }
+
+ return b_t_dist;
+ }
+
+ // The param value gives the relative distance of the given point on the polyline of raw points.
+ // Here, we calculate a point on the bezier curve that is the same relative distance as param.
+ // The 't' for such a point is returned.
+ private double find_t (Geometry.Point[] bez_curve, double param, double[] t_dist, double parts) {
+ if (param < 0) {
+ return 0;
+ }
+
+ if (param > 1) {
+ return 1;
+ }
+
+ for (int i = 1; i <= parts; ++i) {
+
+ if (param <= t_dist[i]) {
+ var t_min = (i - 1) / parts;
+ var t_max = i / parts;
+ var len_min = t_dist[i - 1];
+ var len_max = t_dist[i];
+
+ var t = (param - len_min) / (len_max - len_min) * (t_max - t_min) + t_min;
+ return t;
+ }
+ }
+
+ return 0;
+ }
+
+ private double norm (Geometry.Point p) {
+ return p.distance (Geometry.Point (0, 0));
+ }
+
+ private Geometry.Point normalize (Geometry.Point pt) {
+ var length = norm (pt);
+ return pt.scale (1.0 / length);
+ }
+
+ /*
+ * This method shift all points in path such that none of them are in negative space.
+ */
+ private Geometry.Point[] recalculate_points (Geometry.Point[] points) {
+ double min_x = 0, min_y = 0;
+
+ foreach (var pt in points) {
+ if (pt.x < min_x) {
+ min_x = pt.x;
+ }
+ if (pt.y < min_y) {
+ min_y = pt.y;
+ }
+ }
+
+ Geometry.Point[] recalculated_points = new Geometry.Point[points.length];
+
+ // Shift all the points.
+ for (int i = 0; i < points.length; ++i) {
+ recalculated_points[i] = Geometry.Point (points[i].x - min_x, points[i].y - min_y);
+ }
+
+ // Then shift the reference point.
+ first_point.x += min_x;
+ first_point.y += min_y;
+
+ return recalculated_points;
+ }
+
+ private void recompute_components () {
+ // To calculate the new center of bounds of rectangle,
+ // Move the center to point where user placed first point. This is represented as (0,0) internally.
+ // Then translate it to the relative center of bounding box of path.
+ var bounds = instance.components.path.calculate_extents ();
+ double center_x = first_point.x + bounds.center_x;
+ double center_y = first_point.y + bounds.center_y;
+
+ instance.components.center = new Lib.Components.Coordinates (center_x, center_y);
+ instance.components.size = new Lib.Components.Size (bounds.width, bounds.height, false);
+ // Update the component.
+ view_canvas.items_manager.item_model.mark_node_geometry_dirty_by_id (instance.id);
+ view_canvas.items_manager.compile_model ();
+
+ // After we have computed the path, there is no need to show to raw points.
+ // Update the view without those.
+ update_view (false);
+ }
+
+ /*
+ * Recalculates the extents and updates the ViewLayerPath
+ */
+ private void update_view (bool show_live_path = true) {
+ PathDataModel path_data = PathDataModel ();
+ path_data.source_type = Lib.Modes.AbstractInteractionMode.ModeType.FREE_HAND;
+
+ var points = instance.components.path.data;
+
+ var coordinates = view_canvas.selection_manager.selection.coordinates ();
+
+ Geometry.Rectangle extents = Geometry.Rectangle.empty ();
+ extents.left = coordinates.center_x - coordinates.width / 2.0;
+ extents.right = coordinates.center_x + coordinates.width / 2.0;
+ extents.top = coordinates.center_y - coordinates.height / 2.0;
+ extents.bottom = coordinates.center_y + coordinates.height / 2.0;
+
+ path_data.points = points;
+ path_data.commands = commands;
+
+ if (show_live_path) {
+ path_data.live_pts = raw_points;
+ }
+
+ path_data.length = raw_points.length;
+ path_data.extents = extents;
+ path_data.rot_angle = instance.components.transform.rotation;
+
+ // replaace thhis code from the path segment pr.
+ path_data.live_extents = get_extents_using_live_pts (extents);
+
+ path_layer.update_path_data (path_data);
+ }
+
+ private Geometry.Rectangle get_extents_using_live_pts (Geometry.Rectangle extents) {
+ if (points.length == 0 || raw_points.length == 0) {
+ return extents;
+ }
+
+ var data = new Geometry.Point[raw_points.length + 1];
+
+ data[0] = Geometry.Point ();
+ data[0].x = first_point.x;
+ data[0].y = first_point.y;
+
+ for (int i = 0; i < raw_points.length; ++i) {
+ data[i + 1].x = raw_points[i].x + first_point.x;
+ data[i + 1].y = raw_points[i].y + first_point.y;
+ }
+
+ // The array of commands isn't really needed for calculating extents. So just keep it empty.
+ var cmds = new Lib.Modes.PathEditMode.Type[0];
+ var live_path = new Lib.Components.Path.from_points (data, cmds);
+
+ return live_path.calculate_extents ();
+ }
+}
diff --git a/src/Models/PathEditModel.vala b/src/Models/PathEditModel.vala
index 29a87f413..d0beae2e9 100644
--- a/src/Models/PathEditModel.vala
+++ b/src/Models/PathEditModel.vala
@@ -184,6 +184,7 @@ public class Akira.Models.PathEditModel : Object {
extents.bottom = coordinates.center_y + coordinates.height / 2.0;
PathDataModel path_data = PathDataModel ();
+ path_data.source_type = Lib.Modes.AbstractInteractionMode.ModeType.PATH_EDIT;
path_data.points = points;
path_data.commands = commands;
path_data.live_pts = live_pts;
@@ -239,4 +240,7 @@ public struct Akira.Models.PathDataModel {
public Geometry.Rectangle live_extents;
public double rot_angle;
+
+ // Stores where this path data was recieved from.
+ public Lib.Modes.AbstractInteractionMode.ModeType source_type;
}
diff --git a/src/Services/ActionManager.vala b/src/Services/ActionManager.vala
index 9874c3218..0caea35f0 100644
--- a/src/Services/ActionManager.vala
+++ b/src/Services/ActionManager.vala
@@ -61,6 +61,7 @@ public class Akira.Services.ActionManager : Object {
public const string ACTION_TEXT_TOOL = "action_text_tool";
public const string ACTION_IMAGE_TOOL = "action_image_tool";
public const string ACTION_PATH_TOOL = "action_path_tool";
+ public const string ACTION_PENCIL_TOOL = "action_pencil_tool";
public const string ACTION_DELETE = "action_delete";
public const string ACTION_FLIP_H = "action_flip_h";
public const string ACTION_FLIP_V = "action_flip_v";
@@ -108,6 +109,7 @@ public class Akira.Services.ActionManager : Object {
{ ACTION_TEXT_TOOL, action_text_tool },
{ ACTION_IMAGE_TOOL, action_image_tool },
{ ACTION_PATH_TOOL, action_path_tool },
+ { ACTION_PENCIL_TOOL, action_pencil_tool },
{ ACTION_DELETE, action_delete },
{ ACTION_FLIP_H, action_flip_h },
{ ACTION_FLIP_V, action_flip_v },
@@ -167,6 +169,7 @@ public class Akira.Services.ActionManager : Object {
typing_accelerators.set (ACTION_TEXT_TOOL, "t");
typing_accelerators.set (ACTION_IMAGE_TOOL, "i");
typing_accelerators.set (ACTION_PATH_TOOL, "v");
+ typing_accelerators.set (ACTION_PENCIL_TOOL, "p");
typing_accelerators.set (ACTION_DELETE, "Delete");
typing_accelerators.set (ACTION_DELETE, "BackSpace");
typing_accelerators.set (ACTION_TOGGLE_PIXEL_GRID, "Tab");
@@ -411,6 +414,10 @@ public class Akira.Services.ActionManager : Object {
window.event_bus.insert_item ("path");
}
+ private void action_pencil_tool () {
+ window.event_bus.insert_item ("pencil");
+ }
+
private void on_update_preview () {
string? filename = dialog.get_preview_filename ();
if (filename == null) {
diff --git a/src/ViewLayers/ViewLayerPath.vala b/src/ViewLayers/ViewLayerPath.vala
index 25ef4a0f4..aa6a746a6 100644
--- a/src/ViewLayers/ViewLayerPath.vala
+++ b/src/ViewLayers/ViewLayerPath.vala
@@ -107,7 +107,7 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer {
context.fill ();
++point_idx;
- } else {
+ } else if (commands[i] == Lib.Modes.PathEditMode.Type.CURVE) {
for (int j = 0; j < 4; ++j) {
var pt = points[j + point_idx];
@@ -123,6 +123,27 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer {
context.line_to (points[point_idx + 2].x + reference_point.x, points[point_idx + 2].y + reference_point.y);
context.stroke ();
+ point_idx += 4;
+ } else if (commands[i] == Lib.Modes.PathEditMode.Type.BEZIER) {
+ for (int j = 0; j < 4; ++j) {
+ var pt = points[j + point_idx];
+
+ // Apply the rotation formula and rotate the point by given angle
+ double rot_x = cos_theta * (pt.x - origin.x) - sin_theta * (pt.y - origin.y) + origin.x;
+ double rot_y = sin_theta * (pt.x - origin.x) + cos_theta * (pt.y - origin.y) + origin.y;
+
+ context.arc (rot_x + reference_point.x, rot_y + reference_point.y, radius, 0, Math.PI * 2);
+ context.fill ();
+ }
+
+ context.move_to (points[point_idx].x + reference_point.x, points[point_idx].y + reference_point.y);
+ context.line_to (points[point_idx + 1].x + reference_point.x, points[point_idx + 1].y + reference_point.y);
+
+ context.move_to (points[point_idx + 2].x + reference_point.x, points[point_idx + 2].y + reference_point.y);
+ context.line_to (points[point_idx + 3].x + reference_point.x, points[point_idx + 3].y + reference_point.y);
+
+ context.stroke ();
+
point_idx += 4;
}
}
@@ -150,6 +171,20 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer {
var last_point = points[points.length - 1];
context.move_to (last_point.x + reference_point.x, last_point.y + reference_point.y);
+ if (path_data.source_type == Lib.Modes.AbstractInteractionMode.ModeType.FREE_HAND) {
+ // If there are more than 4 points, we are probably in freehand mode.
+ var first_point = reference_point;
+ foreach (var item in live_pts) {
+ context.line_to (item.x + first_point.x, item.y + first_point.y);
+ }
+
+ context.stroke ();
+ context.new_path ();
+ context.restore ();
+
+ return;
+ }
+
switch (path_data.length) {
case 0:
break;
diff --git a/src/meson.build b/src/meson.build
index 1a043ff84..d0994dd59 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -85,6 +85,7 @@ sources = files(
#'Models/ExportModel.vala',
#'Models/ListModel.vala',
'Models/PathEditModel.vala',
+ 'Models/FreeHandModel.vala',
'Dialogs/ShortcutsDialog.vala',
'Dialogs/SettingsDialog.vala',
@@ -129,6 +130,7 @@ sources = files(
'Lib/Items/ModelTypeGroup.vala',
'Lib/Items/ModelTypeRect.vala',
'Lib/Items/ModelTypePath.vala',
+ 'Lib/Items/ModelTypePencil.vala',
'Lib/Managers/CopyManager.vala',
'Lib/Managers/ItemsManager.vala',
@@ -144,6 +146,7 @@ sources = files(
'Lib/Modes/ExportMode.vala',
'Lib/Modes/PanMode.vala',
'Lib/Modes/PathEditMode.vala',
+ 'Lib/Modes/FreeHandMode.vala',
'Lib/ViewCanvas.vala',