Skip to content

Initial Implementation for Pencil Tool (Freehand Drawing) #679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
17 changes: 17 additions & 0 deletions src/Drawables/DrawablePath.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the difference between CURVE and Bezier--they are both drawing beziers.

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;
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/Geometry/Point.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably add a distance_squared for distance comparisons--so we don't have to do sqrt when not necessary.

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");
Expand Down
6 changes: 5 additions & 1 deletion src/Layouts/HeaderBar.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions src/Layouts/LayersList/LayerItemModel.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
Expand Down
102 changes: 102 additions & 0 deletions src/Lib/Items/ModelTypePencil.vala
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* Authored by: Martin "mbfraga" Fraga <[email protected]>
*/

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;
}
}
}
3 changes: 2 additions & 1 deletion src/Lib/Modes/AbstractInteractionMode.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
99 changes: 99 additions & 0 deletions src/Lib/Modes/FreeHandMode.vala
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* Authored by: Ashish Shevale <[email protected]>
*/

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;
}
}
45 changes: 45 additions & 0 deletions src/Lib/Modes/ItemInsertMode.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 () {
Expand All @@ -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 () {
Expand All @@ -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;
}

Expand All @@ -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);
Expand All @@ -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 ();
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down
Loading