using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; // This is the code for your desktop app. // Press Ctrl+F5 (or go to Debug > Start Without Debugging) to run your app. namespace SketchAssistant { public partial class Form1 : Form { public Form1() { InitializeComponent(); } /**********************************/ /*** CLASS VARIABLES START HERE ***/ /**********************************/ //Different Program States public enum ProgramState { Idle, Draw, Delete } //Current Program State private ProgramState currentState; //Dialog to select a file. OpenFileDialog openFileDialogLeft = new OpenFileDialog(); //Image loaded on the left Image leftImage = null; //Image on the right Image rightImage = null; //Current Line being Drawn List currentLine; //All Lines in the current session List> lineList = new List>(); //Whether the Mouse is currently pressed in the rightPictureBox bool mousePressed = false; //The Position of the Cursor in the right picture box Point currentCursorPosition; //The Previous Cursor Position in the right picture box Point previousCursorPosition; //Queue for the cursorPositions Queue cursorPositions = new Queue(); //The graphic representation of the right image Graphics graph = null; //Deletion Matrixes for checking postions of lines in the image bool[,] isFilledMatrix; HashSet[,] linesMatrix; //Size of deletion area uint deletionSize = 2; //History of Actions ActionHistory historyOfActions; /******************************************/ /*** FORM SPECIFIC FUNCTIONS START HERE ***/ /******************************************/ private void Form1_Load(object sender, EventArgs e) { currentState = ProgramState.Idle; this.DoubleBuffered = true; historyOfActions = new ActionHistory(null); UpdateButtonStatus(); } //Resize Function connected to the form resize event, will refresh the form when it is resized private void Form1_Resize(object sender, System.EventArgs e) { this.Refresh(); } //Load button, will open an OpenFileDialog private void loadToolStripMenuItem_Click(object sender, EventArgs e) { openFileDialogLeft.Filter = "Image|*.jpg;*.png;*.jpeg"; if(openFileDialogLeft.ShowDialog() == DialogResult.OK) { toolStripLoadStatus.Text = openFileDialogLeft.SafeFileName; leftImage = Image.FromFile(openFileDialogLeft.FileName); pictureBoxLeft.Image = leftImage; //Refresh the left image box when the content is changed this.Refresh(); } UpdateButtonStatus(); } //Changes the state of the program to drawing private void drawButton_Click(object sender, EventArgs e) { if(rightImage != null) { if (currentState.Equals(ProgramState.Draw)) { ChangeState(ProgramState.Idle); } else { ChangeState(ProgramState.Draw); } } UpdateButtonStatus(); } //Changes the state of the program to deletion private void deleteButton_Click(object sender, EventArgs e) { if (rightImage != null) { if (currentState.Equals(ProgramState.Delete)) { ChangeState(ProgramState.Idle); } else { ChangeState(ProgramState.Delete); } } UpdateButtonStatus(); } //Undo an action private void undoButton_Click(object sender, EventArgs e) { if (historyOfActions.CanUndo()) { HashSet affectedLines = historyOfActions.GetCurrentAction().GetLineIDs(); SketchAction.ActionType undoAction = historyOfActions.GetCurrentAction().GetActionType(); switch (undoAction) { case SketchAction.ActionType.Delete: //Deleted Lines need to be shown ChangeLines(affectedLines, true); break; case SketchAction.ActionType.Draw: //Drawn lines need to be hidden ChangeLines(affectedLines, false); break; default: break; } } historyOfActions.MoveAction(true); UpdateButtonStatus(); } //Redo an action private void redoButton_Click(object sender, EventArgs e) { if (historyOfActions.CanRedo()) { historyOfActions.MoveAction(false); HashSet affectedLines = historyOfActions.GetCurrentAction().GetLineIDs(); SketchAction.ActionType redoAction = historyOfActions.GetCurrentAction().GetActionType(); switch (redoAction) { case SketchAction.ActionType.Delete: //Deleted Lines need to be redeleted ChangeLines(affectedLines, false); break; case SketchAction.ActionType.Draw: //Drawn lines need to be redrawn ChangeLines(affectedLines, true); break; default: break; } } UpdateButtonStatus(); } //Detect Keyboard Shortcuts private void Form1_KeyDown(object sender, KeyEventArgs e) { if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Z) { undoButton_Click(sender, e); } if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Y) { redoButton_Click(sender, e); } } //get current Mouse positon within the right picture box private void pictureBoxRight_MouseMove(object sender, MouseEventArgs e) { currentCursorPosition = ConvertCoordinates(new Point(e.X, e.Y)); } //hold left mouse button to draw. private void pictureBoxRight_MouseDown(object sender, MouseEventArgs e) { mousePressed = true; if (currentState.Equals(ProgramState.Draw)) { currentLine = new List(); } } //Lift left mouse button to stop drawing and add a new Line. private void pictureBoxRight_MouseUp(object sender, MouseEventArgs e) { mousePressed = false; if (currentState.Equals(ProgramState.Draw) && currentLine.Count > 0) { Line newLine = new Line(currentLine, lineList.Count); lineList.Add(new Tuple(true, newLine)); newLine.PopulateMatrixes(isFilledMatrix, linesMatrix); historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Draw, newLine.GetID())); } UpdateButtonStatus(); } //Button to create a new Canvas. Will create an empty image //which is the size of the left image, if there is one. //If there is no image loaded the canvas will be the size of the right picture box private void canvasButton_Click(object sender, EventArgs e) { if (!historyOfActions.IsEmpty()) { if (MessageBox.Show("You have unsaved changes, creating a new canvas will discard these.", "Attention", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == DialogResult.OK) { historyOfActions = new ActionHistory(lastActionTakenLabel); DrawEmptyCanvas(); //The following lines cannot be in DrawEmptyCanvas() isFilledMatrix = new bool[rightImage.Width, rightImage.Height]; linesMatrix = new HashSet[rightImage.Width, rightImage.Height]; lineList = new List>(); } } else { historyOfActions = new ActionHistory(lastActionTakenLabel); DrawEmptyCanvas(); //The following lines cannot be in DrawEmptyCanvas() isFilledMatrix = new bool[rightImage.Width, rightImage.Height]; linesMatrix = new HashSet[rightImage.Width, rightImage.Height]; lineList = new List>(); } UpdateButtonStatus(); } //add a Point on every tick to the Drawpath private void mouseTimer_Tick(object sender, EventArgs e) { cursorPositions.Enqueue(currentCursorPosition); previousCursorPosition = cursorPositions.Dequeue(); if (currentState.Equals(ProgramState.Draw) && mousePressed) { currentLine.Add(currentCursorPosition); Line drawline = new Line(currentLine); drawline.DrawLine(graph); pictureBoxRight.Image = rightImage; } if (currentState.Equals(ProgramState.Delete) && mousePressed) { List uncheckedPoints = Line.BresenhamLineAlgorithm(previousCursorPosition, currentCursorPosition); foreach (Point currPoint in uncheckedPoints) { HashSet linesToDelete = CheckDeletionMatrixesAroundPoint(currPoint, deletionSize); if (linesToDelete.Count > 0) { historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Delete, linesToDelete)); foreach (int lineID in linesToDelete) { lineList[lineID] = new Tuple(false, lineList[lineID].Item2); } RepopulateDeletionMatrixes(); RedrawRightImage(); } } } } /***********************************/ /*** HELPER FUNCTIONS START HERE ***/ /***********************************/ /// /// Creates an empty Canvas /// private void DrawEmptyCanvas() { if (leftImage == null) { rightImage = new Bitmap(pictureBoxRight.Width, pictureBoxRight.Height); graph = Graphics.FromImage(rightImage); graph.FillRectangle(Brushes.White, 0, 0, pictureBoxRight.Width + 10, pictureBoxRight.Height + 10); pictureBoxRight.Image = rightImage; } else { rightImage = new Bitmap(leftImage.Width, leftImage.Height); graph = Graphics.FromImage(rightImage); graph.FillRectangle(Brushes.White, 0, 0, leftImage.Width + 10, leftImage.Height + 10); pictureBoxRight.Image = rightImage; } this.Refresh(); pictureBoxRight.Refresh(); } /// /// Redraws all lines in lineList, for which their associated boolean value equals true. /// private void RedrawRightImage() { DrawEmptyCanvas(); foreach (Tuple lineBoolTuple in lineList) { if (lineBoolTuple.Item1) { lineBoolTuple.Item2.DrawLine(graph); } } pictureBoxRight.Refresh(); } /// /// Change the status of whether or not the lines are shown. /// /// The HashSet containing the affected Line IDs. /// True if the lines should be shown, false if they should be hidden. private void ChangeLines(HashSet lines, bool shown) { foreach (int lineId in lines) { if (lineId <= lineList.Count - 1 && lineId >= 0) { lineList[lineId] = new Tuple(shown, lineList[lineId].Item2); } } RedrawRightImage(); } /// /// Updates the active status of buttons. Currently draw, delete, undo and redo button. /// private void UpdateButtonStatus() { undoButton.Enabled = historyOfActions.CanUndo(); redoButton.Enabled = historyOfActions.CanRedo(); drawButton.Enabled = (rightImage != null); deleteButton.Enabled = (rightImage != null); } /// /// A helper function which handles tasks associated witch changing states, /// such as checking and unchecking buttons and changing the state. /// /// The new state of the program private void ChangeState(ProgramState newState) { switch (currentState) { case ProgramState.Draw: drawButton.CheckState = CheckState.Unchecked; mouseTimer.Enabled = false; break; case ProgramState.Delete: deleteButton.CheckState = CheckState.Unchecked; mouseTimer.Enabled = false; break; default: break; } switch (newState) { case ProgramState.Draw: drawButton.CheckState = CheckState.Checked; mouseTimer.Enabled = true; break; case ProgramState.Delete: deleteButton.CheckState = CheckState.Checked; mouseTimer.Enabled = true; break; default: break; } currentState = newState; pictureBoxRight.Refresh(); } /// /// A function that calculates the coordinates of a point on a zoomed in image. /// /// The position of the mouse cursor /// The real coordinates of the mouse cursor on the image private Point ConvertCoordinates(Point cursorPosition) { Point realCoordinates = new Point(5,3); if(pictureBoxRight.Image == null) { return cursorPosition; } int widthImage = pictureBoxRight.Image.Width; int heightImage = pictureBoxRight.Image.Height; int widthBox = pictureBoxRight.Width; int heightBox = pictureBoxRight.Height; float imageRatio = (float)widthImage / (float)heightImage; float containerRatio = (float)widthBox / (float)heightBox; if (imageRatio >= containerRatio) { //Image is wider than it is high float zoomFactor = (float)widthImage / (float)widthBox; float scaledHeight = heightImage / zoomFactor; float filler = (heightBox - scaledHeight) / 2; realCoordinates.X = (int)(cursorPosition.X * zoomFactor); realCoordinates.Y = (int)((cursorPosition.Y - filler) * zoomFactor); } else { //Image is higher than it is wide float zoomFactor = (float)heightImage / (float)heightBox; float scaledWidth = widthImage / zoomFactor; float filler = (widthBox - scaledWidth) / 2; realCoordinates.X = (int)((cursorPosition.X - filler) * zoomFactor); realCoordinates.Y = (int)(cursorPosition.Y * zoomFactor); } return realCoordinates; } /// /// A function that populates the matrixes needed for deletion detection with line data. /// private void RepopulateDeletionMatrixes() { if(rightImage != null) { isFilledMatrix = new bool[rightImage.Width,rightImage.Height]; linesMatrix = new HashSet[rightImage.Width, rightImage.Height]; foreach(Tuple lineTuple in lineList) { if (lineTuple.Item1) { lineTuple.Item2.PopulateMatrixes(isFilledMatrix, linesMatrix); } } } } /// /// A function that checks the deletion matrixes at a certain point /// and returns all Line ids at that point and in a square around it in a certain range. /// /// The point around which to check. /// The range around the point. If range is 0, only the point is checked. /// A List of all lines. private HashSet CheckDeletionMatrixesAroundPoint(Point p, uint range) { HashSet returnSet = new HashSet(); if (p.X >= 0 && p.Y >= 0 && p.X < rightImage.Width && p.Y < rightImage.Height) { if (isFilledMatrix[p.X, p.Y]) { returnSet.UnionWith(linesMatrix[p.X, p.Y]); } } for (int x_mod = (int)range*(-1); x_mod < range; x_mod++) { for (int y_mod = (int)range * (-1); y_mod < range; y_mod++) { if (p.X + x_mod >= 0 && p.Y + y_mod >= 0 && p.X + x_mod < rightImage.Width && p.Y + y_mod < rightImage.Height) { if (isFilledMatrix[p.X + x_mod, p.Y + y_mod]) { returnSet.UnionWith(linesMatrix[p.X + x_mod, p.Y + y_mod]); } } } } return returnSet; } } }