From a99ad948a58150937382d41fad0da6de27268c4d Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Mon, 13 Dec 2021 21:42:15 +0530 Subject: [PATCH 1/9] Create basic FreeHandMode for creating curves --- src/Lib/Modes/AbstractInteractionMode.vala | 3 +- src/Lib/Modes/FreeHandMode.vala | 82 ++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/Lib/Modes/FreeHandMode.vala 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..dace50899 --- /dev/null +++ b/src/Lib/Modes/FreeHandMode.vala @@ -0,0 +1,82 @@ +/** + * 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.PathEditModel edit_model; + + public FreeHandMode (Lib.ViewCanvas canvas, Lib.Items.ModelInstance instance) { + Object ( + view_canvas: canvas, + instance: instance + ); + } + + 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) { + return true; + } + + public override bool button_release_event (Gdk.EventButton event) { + return true; + } + + public override bool motion_notify_event (Gdk.EventMotion event) { + return true; + } + + public override Object? extra_context () { + return null; + } +} From 6859e19f31c2f2d0fcb0d87f20d4c532d1cf6d70 Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Mon, 13 Dec 2021 22:00:56 +0530 Subject: [PATCH 2/9] Setup actions and create dummy item --- src/Layouts/HeaderBar.vala | 6 +++- src/Lib/Modes/FreeHandMode.vala | 2 ++ src/Lib/Modes/ItemInsertMode.vala | 50 +++++++++++++++++++++++++++++++ src/Services/ActionManager.vala | 7 +++++ src/meson.build | 1 + 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Layouts/HeaderBar.vala b/src/Layouts/HeaderBar.vala index f5eb89bff..5fec63133 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/Lib/Modes/FreeHandMode.vala b/src/Lib/Modes/FreeHandMode.vala index dace50899..d0d63e8c0 100644 --- a/src/Lib/Modes/FreeHandMode.vala +++ b/src/Lib/Modes/FreeHandMode.vala @@ -30,6 +30,8 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { view_canvas: canvas, instance: instance ); + + edit_model = new Models.PathEditModel (instance, view_canvas); } public override AbstractInteractionMode.ModeType mode_type () { diff --git a/src/Lib/Modes/ItemInsertMode.vala b/src/Lib/Modes/ItemInsertMode.vala index ce6caecb2..0e72b1ac3 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,8 +269,32 @@ 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.ModelTypePath.default_path ( + coordinates, + borders_from_settings (), + null + ); + + var test_path = new Geometry.Point[6]; + test_path[0] = Geometry.Point (0, 0); + test_path[1] = Geometry.Point (10, 40); + test_path[2] = Geometry.Point (50, 200); + test_path[3] = Geometry.Point (100, 40); + test_path[4] = Geometry.Point (30, 40); + test_path[5] = Geometry.Point (10, 10); + + var line = Lib.Modes.PathEditMode.Type.LINE; + Lib.Modes.PathEditMode.Type[] commands = { line, line, line, line, line, line }; + new_item.components.path = new Lib.Components.Path.from_points (test_path, commands, false); break; } 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/meson.build b/src/meson.build index 1a043ff84..b1b87002d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -144,6 +144,7 @@ sources = files( 'Lib/Modes/ExportMode.vala', 'Lib/Modes/PanMode.vala', 'Lib/Modes/PathEditMode.vala', + 'Lib/Modes/FreeHandMode.vala', 'Lib/ViewCanvas.vala', From 0be8f0dbe6b6a227ddf519f5456bfbabc3a42c4c Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Mon, 13 Dec 2021 22:12:53 +0530 Subject: [PATCH 3/9] Proper name and icon in Layer panel --- src/Layouts/LayersList/LayerItemModel.vala | 2 + src/Lib/Items/ModelTypePencil.vala | 102 +++++++++++++++++++++ src/Lib/Modes/ItemInsertMode.vala | 2 +- src/meson.build | 1 + 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/Lib/Items/ModelTypePencil.vala 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/ItemInsertMode.vala b/src/Lib/Modes/ItemInsertMode.vala index 0e72b1ac3..bbdc33d17 100644 --- a/src/Lib/Modes/ItemInsertMode.vala +++ b/src/Lib/Modes/ItemInsertMode.vala @@ -278,7 +278,7 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode { 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.ModelTypePath.default_path ( + new_item = Lib.Items.ModelTypePencil.default_path ( coordinates, borders_from_settings (), null diff --git a/src/meson.build b/src/meson.build index b1b87002d..088821e3b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -129,6 +129,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', From bd7ad1443bd91329bc55b889ad5919e7d385427b Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Mon, 13 Dec 2021 22:40:07 +0530 Subject: [PATCH 4/9] Create model to store and process path data --- src/Lib/Modes/FreeHandMode.vala | 12 +++- src/Lib/Modes/ItemInsertMode.vala | 11 +-- src/Models/FreeHandModel.vala | 112 ++++++++++++++++++++++++++++++ src/meson.build | 1 + 4 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 src/Models/FreeHandModel.vala diff --git a/src/Lib/Modes/FreeHandMode.vala b/src/Lib/Modes/FreeHandMode.vala index d0d63e8c0..c9ee40a2b 100644 --- a/src/Lib/Modes/FreeHandMode.vala +++ b/src/Lib/Modes/FreeHandMode.vala @@ -23,7 +23,9 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { public weak Lib.ViewCanvas view_canvas { get; construct; } public Lib.Items.ModelInstance instance { get; construct; } - private Models.PathEditModel edit_model; + private Models.FreeHandModel edit_model; + + private bool is_click = false; public FreeHandMode (Lib.ViewCanvas canvas, Lib.Items.ModelInstance instance) { Object ( @@ -31,7 +33,7 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { instance: instance ); - edit_model = new Models.PathEditModel (instance, view_canvas); + edit_model = new Models.FreeHandModel (instance, view_canvas); } public override AbstractInteractionMode.ModeType mode_type () { @@ -67,10 +69,16 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { } 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; return true; } diff --git a/src/Lib/Modes/ItemInsertMode.vala b/src/Lib/Modes/ItemInsertMode.vala index bbdc33d17..44b4bb070 100644 --- a/src/Lib/Modes/ItemInsertMode.vala +++ b/src/Lib/Modes/ItemInsertMode.vala @@ -284,16 +284,11 @@ public class Akira.Lib.Modes.ItemInsertMode : AbstractInteractionMode { null ); - var test_path = new Geometry.Point[6]; + var test_path = new Geometry.Point[1]; test_path[0] = Geometry.Point (0, 0); - test_path[1] = Geometry.Point (10, 40); - test_path[2] = Geometry.Point (50, 200); - test_path[3] = Geometry.Point (100, 40); - test_path[4] = Geometry.Point (30, 40); - test_path[5] = Geometry.Point (10, 10); - var line = Lib.Modes.PathEditMode.Type.LINE; - Lib.Modes.PathEditMode.Type[] commands = { line, line, line, line, line, line }; + + 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; diff --git a/src/Models/FreeHandModel.vala b/src/Models/FreeHandModel.vala new file mode 100644 index 000000000..1b6e27e0a --- /dev/null +++ b/src/Models/FreeHandModel.vala @@ -0,0 +1,112 @@ +/** + * 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; + + 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; + + // 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 (); + } + + /* + * Recalculates the extents and updates the ViewLayerPath + */ + private void update_view () { + 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; + + PathDataModel path_data = PathDataModel (); + path_data.points = points; + path_data.commands = commands; + path_data.live_pts = raw_points; + path_data.length = raw_points.length; + path_data.extents = extents; + path_data.rot_angle = instance.components.transform.rotation; + + 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) { + var live_extents = Geometry.Rectangle.empty (); + + live_extents.left = extents.left; + live_extents.right = extents.right; + live_extents.top = extents.top; + live_extents.bottom = extents.bottom; + + for (int i = 0; i < raw_points.length; ++i) { + var temp = raw_points[i]; + temp.x = temp.x - first_point.x + extents.left; + temp.y = temp.y - first_point.y + extents.top; + + if (temp.x < extents.left) { + live_extents.left = temp.x; + } + if (temp.x > extents.right) { + live_extents.right = temp.x; + } + if (temp.y < extents.top) { + live_extents.top = temp.y; + } + if (temp.y > extents.bottom) { + live_extents.bottom = temp.y; + } + } + + return live_extents; + } +} diff --git a/src/meson.build b/src/meson.build index 088821e3b..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', From 86ca51036495f68970e89762ff5c1aa58b136e8f Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Mon, 13 Dec 2021 23:04:24 +0530 Subject: [PATCH 5/9] Store raw points from user events --- src/Lib/Modes/FreeHandMode.vala | 5 +++++ src/Models/FreeHandModel.vala | 9 +++++++++ src/ViewLayers/ViewLayerPath.vala | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/src/Lib/Modes/FreeHandMode.vala b/src/Lib/Modes/FreeHandMode.vala index c9ee40a2b..a6d427231 100644 --- a/src/Lib/Modes/FreeHandMode.vala +++ b/src/Lib/Modes/FreeHandMode.vala @@ -83,6 +83,11 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { } 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; } diff --git a/src/Models/FreeHandModel.vala b/src/Models/FreeHandModel.vala index 1b6e27e0a..a4d10a1e7 100644 --- a/src/Models/FreeHandModel.vala +++ b/src/Models/FreeHandModel.vala @@ -46,6 +46,8 @@ public class Akira.Models.FreeHandModel : Object { 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); @@ -53,6 +55,13 @@ public class Akira.Models.FreeHandModel : Object { update_view (); } + public void add_raw_point (Geometry.Point point) { + raw_points.resize (raw_points.length + 1); + raw_points[raw_points.length - 1] = Geometry.Point (point.x, point.y); + + update_view (); + } + /* * Recalculates the extents and updates the ViewLayerPath */ diff --git a/src/ViewLayers/ViewLayerPath.vala b/src/ViewLayers/ViewLayerPath.vala index 25ef4a0f4..360d8ea4c 100644 --- a/src/ViewLayers/ViewLayerPath.vala +++ b/src/ViewLayers/ViewLayerPath.vala @@ -62,6 +62,7 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer { return; } + print("requesting redraw\n"); canvas.request_redraw (old_live_extents); canvas.request_redraw (path_data.live_extents); old_live_extents = path_data.live_extents; @@ -222,6 +223,12 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer { break; default: + // If there are more than 4 points, we are probably in freehand mode. + foreach (var item in live_pts) { + context.line_to (item.x, item.y); + } + + context.stroke (); break; } From 814764b3f799d09cb3a70c330f02626870f72fac Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Sat, 18 Dec 2021 12:06:29 +0530 Subject: [PATCH 6/9] Write Schneider's Algorithm for simplifying the raw points --- src/Models/FreeHandModel.vala | 436 ++++++++++++++++++++++++++++++++-- 1 file changed, 410 insertions(+), 26 deletions(-) diff --git a/src/Models/FreeHandModel.vala b/src/Models/FreeHandModel.vala index a4d10a1e7..ecd6bccf9 100644 --- a/src/Models/FreeHandModel.vala +++ b/src/Models/FreeHandModel.vala @@ -35,6 +35,43 @@ public class Akira.Models.FreeHandModel : Object { // 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, @@ -57,15 +94,363 @@ public class Akira.Models.FreeHandModel : Object { public void add_raw_point (Geometry.Point point) { raw_points.resize (raw_points.length + 1); - raw_points[raw_points.length - 1] = Geometry.Point (point.x, point.y); + 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) { + // The maximum error permissible before we try to divide and recurse. + var iteration_error = TOLERANCE * TOLERANCE; + // Number of times we will make adjustments in the points and try to refit before recursing. + var max_iterations = 20; + + // 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 (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; + } + } + + var center_tan = pts[split - 1].sub (pts[split + 1]); + 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); + var from_center_tangent = to_center_tangent.scale (-1); + + 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; + } + + // Uses Least Squares Method to find bezier control points for a region. + 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]; + + 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]; + double[] X = new double[2]; + 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]; + var det_C0_X = C[0, 0] * X[1] - C[1, 0] * X[0]; + var det_X_C1 = X[0] * C[1, 1] - X[1] * C[0, 1]; + + 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 negative, use the Wu/Barsky heuristic. + //If alpha is 0, you get coincident control points that lead to + //divide by zero in any subsequent new_raphson_root_find() call. + 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; + } + + 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)); + } + + 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; + } + + 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; + } + + 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 () { + 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 (); @@ -76,46 +461,45 @@ public class Akira.Models.FreeHandModel : Object { extents.top = coordinates.center_y - coordinates.height / 2.0; extents.bottom = coordinates.center_y + coordinates.height / 2.0; - PathDataModel path_data = PathDataModel (); path_data.points = points; path_data.commands = commands; - path_data.live_pts = raw_points; + + 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) { - var live_extents = Geometry.Rectangle.empty (); + if (points.length == 0 || raw_points.length == 0) { + return extents; + } - live_extents.left = extents.left; - live_extents.right = extents.right; - live_extents.top = extents.top; - live_extents.bottom = extents.bottom; + var data = new Geometry.Point[raw_points.length + 1]; - for (int i = 0; i < raw_points.length; ++i) { - var temp = raw_points[i]; - temp.x = temp.x - first_point.x + extents.left; - temp.y = temp.y - first_point.y + extents.top; + data[0] = Geometry.Point (); + data[0].x = first_point.x; + data[0].y = first_point.y; - if (temp.x < extents.left) { - live_extents.left = temp.x; - } - if (temp.x > extents.right) { - live_extents.right = temp.x; - } - if (temp.y < extents.top) { - live_extents.top = temp.y; - } - if (temp.y > extents.bottom) { - live_extents.bottom = temp.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; } - return live_extents; + // 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); + + var ext = live_path.calculate_extents (); + + return live_path.calculate_extents (); } } From eaa1e7f90062f42a6ddcc9b68cbe17cf1b98207a Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Sat, 18 Dec 2021 12:07:08 +0530 Subject: [PATCH 7/9] Diplay calculated bezier points on canvas --- src/Drawables/DrawablePath.vala | 17 ++++++++++++ src/Geometry/Point.vala | 20 ++++++++++++++ src/Lib/Modes/FreeHandMode.vala | 1 + src/Lib/Modes/PathEditMode.vala | 3 ++- src/Models/PathEditModel.vala | 4 +++ src/ViewLayers/ViewLayerPath.vala | 45 +++++++++++++++++++++++++------ 6 files changed, 81 insertions(+), 9 deletions(-) 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..94a5b120a 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/Lib/Modes/FreeHandMode.vala b/src/Lib/Modes/FreeHandMode.vala index a6d427231..94db135d2 100644 --- a/src/Lib/Modes/FreeHandMode.vala +++ b/src/Lib/Modes/FreeHandMode.vala @@ -79,6 +79,7 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { public override bool button_release_event (Gdk.EventButton event) { is_click = false; + edit_model.fit_curve (); return true; } 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/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/ViewLayers/ViewLayerPath.vala b/src/ViewLayers/ViewLayerPath.vala index 360d8ea4c..3ac2b1033 100644 --- a/src/ViewLayers/ViewLayerPath.vala +++ b/src/ViewLayers/ViewLayerPath.vala @@ -62,7 +62,6 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer { return; } - print("requesting redraw\n"); canvas.request_redraw (old_live_extents); canvas.request_redraw (path_data.live_extents); old_live_extents = path_data.live_extents; @@ -108,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]; @@ -124,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; } } @@ -151,6 +171,21 @@ 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) { + print("drae live effeect\n"); + // If there are more than 4 points, we are probably in freehand mode. + var first_point = reference_point;//Geometry.Point (path_data.live_extents.left, path_data.live_extents.top); + 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; @@ -223,12 +258,6 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer { break; default: - // If there are more than 4 points, we are probably in freehand mode. - foreach (var item in live_pts) { - context.line_to (item.x, item.y); - } - - context.stroke (); break; } From 324311b6fbe9923d9754679818383ec49296104b Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Sat, 18 Dec 2021 12:16:54 +0530 Subject: [PATCH 8/9] Lint fixes --- src/Geometry/Point.vala | 4 +- src/Layouts/HeaderBar.vala | 4 +- src/Models/FreeHandModel.vala | 63 +++++++++++++++---------------- src/ViewLayers/ViewLayerPath.vala | 3 +- 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/Geometry/Point.vala b/src/Geometry/Point.vala index 94a5b120a..6c8f861a4 100644 --- a/src/Geometry/Point.vala +++ b/src/Geometry/Point.vala @@ -38,11 +38,11 @@ public struct Akira.Geometry.Point { 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); } diff --git a/src/Layouts/HeaderBar.vala b/src/Layouts/HeaderBar.vala index 5fec63133..300af47b7 100644 --- a/src/Layouts/HeaderBar.vala +++ b/src/Layouts/HeaderBar.vala @@ -294,8 +294,8 @@ public class Akira.Layouts.HeaderBar : Gtk.HeaderBar { Akira.Services.ActionManager.ACTION_PATH_TOOL); var pencil = create_model_button ( - _("Pencil"), - "edit-symbolic", + _("Pencil"), + "edit-symbolic", Akira.Services.ActionManager.ACTION_PREFIX + Akira.Services.ActionManager.ACTION_PENCIL_TOOL); diff --git a/src/Models/FreeHandModel.vala b/src/Models/FreeHandModel.vala index ecd6bccf9..3aa01f95d 100644 --- a/src/Models/FreeHandModel.vala +++ b/src/Models/FreeHandModel.vala @@ -43,22 +43,21 @@ public class Akira.Models.FreeHandModel : Object { // 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); - + 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); - - + 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); } @@ -67,7 +66,7 @@ public class Akira.Models.FreeHandModel : Object { 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); } } @@ -142,16 +141,16 @@ public class Akira.Models.FreeHandModel : Object { 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; } @@ -160,7 +159,7 @@ public class Akira.Models.FreeHandModel : Object { // 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; @@ -231,17 +230,17 @@ public class Akira.Models.FreeHandModel : Object { bez_curve[3] = pts[last]; var n_pts = last - first + 1; - Geometry.Point[,] A = new Geometry.Point[n_pts, 2]; - + 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); + A[i, 1] = right_tan.scale (3 * u * u * ux); } - double[,] C = new double[2, 2]; - double[] X = new double[2]; + 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; @@ -255,15 +254,15 @@ public class Akira.Models.FreeHandModel : Object { 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]; - var det_C0_X = C[0, 0] * X[1] - C[1, 0] * X[0]; - var det_X_C1 = X[0] * C[1, 1] - X[1] * C[0, 1]; + 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); @@ -303,8 +302,8 @@ public class Akira.Models.FreeHandModel : Object { 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); + 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); @@ -327,7 +326,7 @@ public class Akira.Models.FreeHandModel : Object { 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) { @@ -343,7 +342,7 @@ public class Akira.Models.FreeHandModel : Object { var b_t_dist = new double[1]; b_t_dist[0] = 0; - + var b_t_prev = bez_curve[0]; var sum_len = 0.0; @@ -378,7 +377,7 @@ public class Akira.Models.FreeHandModel : Object { 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; } @@ -390,7 +389,7 @@ public class Akira.Models.FreeHandModel : Object { 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); @@ -498,8 +497,6 @@ public class Akira.Models.FreeHandModel : Object { var cmds = new Lib.Modes.PathEditMode.Type[0]; var live_path = new Lib.Components.Path.from_points (data, cmds); - var ext = live_path.calculate_extents (); - - return live_path.calculate_extents (); + return live_path.calculate_extents (); } } diff --git a/src/ViewLayers/ViewLayerPath.vala b/src/ViewLayers/ViewLayerPath.vala index 3ac2b1033..aa6a746a6 100644 --- a/src/ViewLayers/ViewLayerPath.vala +++ b/src/ViewLayers/ViewLayerPath.vala @@ -172,9 +172,8 @@ public class Akira.ViewLayers.ViewLayerPath : ViewLayer { 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) { - print("drae live effeect\n"); // If there are more than 4 points, we are probably in freehand mode. - var first_point = reference_point;//Geometry.Point (path_data.live_extents.left, path_data.live_extents.top); + var first_point = reference_point; foreach (var item in live_pts) { context.line_to (item.x + first_point.x, item.y + first_point.y); } From 543adc09dc97c77083706ae3202ae465f6e939ef Mon Sep 17 00:00:00 2001 From: Ashish Shevale Date: Sat, 18 Dec 2021 18:39:10 +0530 Subject: [PATCH 9/9] Helpful comments --- src/Lib/Modes/FreeHandMode.vala | 1 + src/Models/FreeHandModel.vala | 65 +++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/Lib/Modes/FreeHandMode.vala b/src/Lib/Modes/FreeHandMode.vala index 94db135d2..7a6afe3bb 100644 --- a/src/Lib/Modes/FreeHandMode.vala +++ b/src/Lib/Modes/FreeHandMode.vala @@ -80,6 +80,7 @@ public class Akira.Lib.Modes.FreeHandMode : AbstractInteractionMode { 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; } diff --git a/src/Models/FreeHandModel.vala b/src/Models/FreeHandModel.vala index 3aa01f95d..315d89615 100644 --- a/src/Models/FreeHandModel.vala +++ b/src/Models/FreeHandModel.vala @@ -127,11 +127,16 @@ public class Akira.Models.FreeHandModel : Object { * 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) { - // The maximum error permissible before we try to divide and recurse. - var iteration_error = TOLERANCE * TOLERANCE; + 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 = 20; + 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) { @@ -173,6 +178,8 @@ public class Akira.Models.FreeHandModel : Object { 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) { @@ -185,7 +192,13 @@ public class Akira.Models.FreeHandModel : Object { } } + // 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; @@ -193,8 +206,10 @@ public class Akira.Models.FreeHandModel : Object { } 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; @@ -223,8 +238,18 @@ public class Akira.Models.FreeHandModel : Object { return u; } - // Uses Least Squares Method to find bezier control points for a region. - private Geometry.Point[] generate_bezier (Geometry.Point[] pts, int first, int last, double[] u_prime, Geometry.Point left_tan, Geometry.Point right_tan) { + // 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]; @@ -270,9 +295,6 @@ public class Akira.Models.FreeHandModel : Object { var seg_length = pts[last].distance (pts[first]); var epsilon = 1.0e-6 * seg_length; - //If alpha negative, use the Wu/Barsky heuristic. - //If alpha is 0, you get coincident control points that lead to - //divide by zero in any subsequent new_raphson_root_find() call. 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. @@ -292,7 +314,16 @@ public class Akira.Models.FreeHandModel : Object { return bez_curve; } - private double[] reparameterize (Geometry.Point[] pts, int first, int last, double[] u, Geometry.Point[] 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) { @@ -316,7 +347,15 @@ public class Akira.Models.FreeHandModel : Object { return (u - (numerator / denominator)); } - private double compute_max_error (Geometry.Point[] pts, int first, int last, Geometry.Point[] bez_curve, double[] u, out int split) { + // 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; @@ -338,6 +377,7 @@ public class Akira.Models.FreeHandModel : Object { 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]; @@ -361,6 +401,9 @@ public class Akira.Models.FreeHandModel : Object { 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;