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; using System.Text.RegularExpressions; // 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(); fileImporter = new FileImporter(this); } /**********************************/ /*** CLASS VARIABLES START HERE ***/ /**********************************/ //important: add new variables only at the end of the list to keep the order of definition consistent with the order in which they are returned by GetAllVariables() /// /// Different Program States /// public enum ProgramState { Idle, Draw, Delete } /// /// Current Program State /// private ProgramState currentState; /// /// instance of FileImporter to handle drawing imports /// private FileImporter fileImporter; /// /// Dialog to select a file. /// OpenFileDialog openFileDialog = new OpenFileDialog(); /// /// Image loaded on the left /// private Image leftImage = null; /// /// the graphic shown in the left window, represented as a list of polylines /// private List leftLineList; /// /// Image on the right /// Image rightImage = null; /// /// Current Line being Drawn /// List currentLine; /// /// All Lines in the current session /// List> rightLineList = 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 rightGraph = null; /// /// Lookup Matrix for checking postions of lines in the image /// bool[,] isFilledMatrix; /// /// Lookup Matrix for getting line ids at a certain postions of the image /// HashSet[,] linesMatrix; /// /// Size of deletion area /// int deletionRadius = 2; /// /// History of Actions /// ActionHistory historyOfActions; /// /// List of items which will be overlayed over the right canvas. /// List> overlayItems; /// /// The assistant responsible for the redraw mode /// RedrawAssistant redrawAss; /// /// Size of areas marking endpoints of lines in the redraw mode. /// int markerRadius = 10; /******************************************/ /*** FORM SPECIFIC FUNCTIONS START HERE ***/ /******************************************/ private void Form1_Load(object sender, EventArgs e) { currentState = ProgramState.Idle; this.DoubleBuffered = true; historyOfActions = new ActionHistory(null); redrawAss = new RedrawAssistant(); 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(); UpdateSizes(); } //Load button, will open an OpenFileDialog private void loadToolStripMenuItem_Click(object sender, EventArgs e) { openFileDialog.Filter = "Image|*.jpg;*.png;*.jpeg"; if(openFileDialog.ShowDialog() == DialogResult.OK) { toolStripLoadStatus.Text = openFileDialog.SafeFileName; leftImage = Image.FromFile(openFileDialog.FileName); pictureBoxLeft.Image = leftImage; //Refresh the left image box when the content is changed this.Refresh(); } UpdateButtonStatus(); } /// /// Import button, will open an OpenFileDialog /// private void examplePictureToolStripMenuItem_Click(object sender, EventArgs e) { if (CheckSavedStatus()) { openFileDialog.Filter = "Interactive Sketch-Assistant Drawing|*.isad"; if (openFileDialog.ShowDialog() == DialogResult.OK) { toolStripLoadStatus.Text = openFileDialog.SafeFileName; try { (int, int, List) values = fileImporter.ParseISADInputFile(openFileDialog.FileName); DrawEmptyCanvasLeft(values.Item1, values.Item2); BindAndDrawLeftImage(values.Item3); //Match The right canvas to the left historyOfActions = new ActionHistory(lastActionTakenLabel); DrawEmptyCanvasRight(); isFilledMatrix = new bool[rightImage.Width, rightImage.Height]; linesMatrix = new HashSet[rightImage.Width, rightImage.Height]; rightLineList = new List>(); //Start the redraw mode redrawAss = new RedrawAssistant(leftLineList); UpdateSizes(); overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false); RedrawRightImage(); this.Refresh(); } catch (FileImporterException ex) { ShowInfoMessage(ex.ToString()); } } } 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; } overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false); RedrawRightImage(); } 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; } overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false); RedrawRightImage(); } 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 start drawing. /// 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, rightLineList.Count); rightLineList.Add(new Tuple(true, newLine)); newLine.PopulateMatrixes(isFilledMatrix, linesMatrix); historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Draw, newLine.GetID())); //Execute a RedrawAssistant tick with the currently finished Line overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, newLine.GetID(), true); RedrawRightImage(); } 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 (CheckSavedStatus()) { historyOfActions = new ActionHistory(lastActionTakenLabel); DrawEmptyCanvasRight(); //The following lines cannot be in DrawEmptyCanvas() isFilledMatrix = new bool[rightImage.Width, rightImage.Height]; linesMatrix = new HashSet[rightImage.Width, rightImage.Height]; rightLineList = new List>(); //Reinitialise the Redraw Assistant. if(leftLineList != null) { redrawAss = new RedrawAssistant(leftLineList); UpdateSizes(); overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false); RedrawRightImage(); } } UpdateButtonStatus(); UpdateSizes(); } /// /// Add a Point on every tick to the Drawpath. /// Or detect lines for deletion on every tick /// private void mouseTimer_Tick(object sender, EventArgs e) { if(cursorPositions.Count > 0) { previousCursorPosition = cursorPositions.Dequeue(); } else { previousCursorPosition = currentCursorPosition; } cursorPositions.Enqueue(currentCursorPosition); //Drawing if (currentState.Equals(ProgramState.Draw) && mousePressed) { rightGraph = Graphics.FromImage(rightImage); currentLine.Add(currentCursorPosition); Line drawline = new Line(currentLine); drawline.DrawLine(rightGraph); pictureBoxRight.Image = rightImage; //Redraw overlay gets ticked overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, rightLineList.Count, false); RedrawRightImage(); } //Deleting if (currentState.Equals(ProgramState.Delete) && mousePressed) { List uncheckedPoints = GeometryCalculator.BresenhamLineAlgorithm(previousCursorPosition, currentCursorPosition); foreach (Point currPoint in uncheckedPoints) { HashSet linesToDelete = CheckDeletionMatrixesAroundPoint(currPoint, deletionRadius); if (linesToDelete.Count > 0) { historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Delete, linesToDelete)); foreach (int lineID in linesToDelete) { rightLineList[lineID] = new Tuple(false, rightLineList[lineID].Item2); } RepopulateDeletionMatrixes(); //Redraw overlay gets ticked overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false); RedrawRightImage(); } } } } /***********************************/ /*** HELPER FUNCTIONS START HERE ***/ /***********************************/ /// /// A function that returns a white canvas for a given width and height. /// /// The width of the canvas in pixels /// The height of the canvas in pixels /// The new canvas private Image GetEmptyCanvas(int width, int height) { Image image; try { image = new Bitmap(width, height); } catch(ArgumentException e) { ShowInfoMessage("The requested canvas size caused an error: \n" + e.ToString() + "\n The Canvas will be set to match your window."); image = new Bitmap(pictureBoxLeft.Width, pictureBoxLeft.Height); } Graphics graph = Graphics.FromImage(image); graph.FillRectangle(Brushes.White, 0, 0, width + 10, height + 10); return image; } /// /// Creates an empty Canvas /// private void DrawEmptyCanvasRight() { if (leftImage == null) { SetAndRefreshRightImage(GetEmptyCanvas(pictureBoxRight.Width, pictureBoxRight.Height)); } else { SetAndRefreshRightImage(GetEmptyCanvas(leftImage.Width, leftImage.Height)); } } /// /// Creates an empty Canvas on the left /// /// width of the new canvas in pixels /// height of the new canvas in pixels private void DrawEmptyCanvasLeft(int width, int height) { if (width == 0) { SetAndRefreshLeftImage(GetEmptyCanvas(pictureBoxLeft.Width, pictureBoxLeft.Height)); } else { SetAndRefreshLeftImage(GetEmptyCanvas(width, height)); } } /// /// Redraws all lines in lineList, for which their associated boolean value equals true. /// private void RedrawRightImage() { var workingCanvas = GetEmptyCanvas(rightImage.Width, rightImage.Height); var workingGraph = Graphics.FromImage(workingCanvas); //Lines foreach (Tuple lineBoolTuple in rightLineList) { if (lineBoolTuple.Item1) { lineBoolTuple.Item2.DrawLine(workingGraph); } } //The Line being currently drawn if(currentLine != null && currentLine.Count > 0 && currentState.Equals(ProgramState.Draw) && mousePressed) { var currLine = new Line(currentLine); currLine.DrawLine(workingGraph); } //Overlay Items foreach (HashSet item in overlayItems) { foreach(Point p in item) { workingGraph.FillRectangle(Brushes.Green, p.X, p.Y, 1, 1); } } SetAndRefreshRightImage(workingCanvas); } /// /// A function to set rightImage and to refresh the respective PictureBox with it. /// /// The new Image private void SetAndRefreshRightImage(Image image) { rightImage = image; pictureBoxRight.Image = rightImage; pictureBoxRight.Refresh(); } /// /// A function to set leftImage and to refresh the respective PictureBox with it. /// /// The new Image private void SetAndRefreshLeftImage(Image image) { leftImage = image; pictureBoxLeft.Image = leftImage; pictureBoxLeft.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 <= rightLineList.Count - 1 && lineId >= 0) { rightLineList[lineId] = new Tuple(shown, rightLineList[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 rightLineList) { 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, int range) { HashSet returnSet = new HashSet(); foreach(Point pnt in GeometryCalculator.FilledCircleAlgorithm(p, (int)range)) { if(pnt.X >= 0 && pnt.Y >= 0 && pnt.X < rightImage.Width && pnt.Y < rightImage.Height) { if (isFilledMatrix[pnt.X, pnt.Y]) { returnSet.UnionWith(linesMatrix[pnt.X, pnt.Y]); } } } return returnSet; } /// /// binds the given picture to templatePicture and draws it /// /// the new template picture, represented as a list of polylines /// private void BindAndDrawLeftImage(List newTemplatePicture) { leftLineList = newTemplatePicture; foreach(Line l in leftLineList) { l.DrawLine(Graphics.FromImage(leftImage)); } } /// /// shows the given info message in a popup and asks the user to aknowledge it /// /// the message to show private void ShowInfoMessage(String message) { MessageBox.Show(message); } /// /// Will calculate the start and endpoints of the given line on the right canvas. /// /// The line. /// The size of the circle with which the endpoints of the line are marked. private Tuple, HashSet> CalculateStartAndEnd(Line line, int size) { var circle0 = GeometryCalculator.FilledCircleAlgorithm(line.GetStartPoint(), size); var circle1 = GeometryCalculator.FilledCircleAlgorithm(line.GetEndPoint(), size); var currentLineEndings = new Tuple, HashSet>(circle0, circle1); return currentLineEndings; } /// /// A helper Function that updates the markerRadius & deletionRadius, considering the size of the canvas. /// private void UpdateSizes() { if (rightImage != null) { 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; float zoomFactor = 0; if (imageRatio >= containerRatio) { //Image is wider than it is high zoomFactor = (float)widthImage / (float)widthBox; } else { //Image is higher than it is wide zoomFactor = (float)heightImage / (float)heightBox; } markerRadius = (int)(10 * zoomFactor); redrawAss.SetMarkerRadius(markerRadius); deletionRadius = (int)(5 * zoomFactor); } } /// /// Checks if there is unsaved progess, and warns the user. Returns True if it safe to continue. /// /// true if there is none, or the user wishes to continue without saving. /// false if there is progress, and the user doesn't wish to continue. private bool CheckSavedStatus() { if (!historyOfActions.IsEmpty()) { return (MessageBox.Show("You have unsaved changes, do you wish to continue?", "Attention", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes); } return true; } /********************************************/ /*** TESTING RELATED FUNCTIONS START HERE ***/ /********************************************/ /// /// returns all instance variables in the order of their definition for testing /// /// A list of tuples containing names of variables and the variable themselves. /// Cast according to the Type definitions in the class variable section. public List>GetAllVariables() { var objArr = new (String, object)[] { ("currentState", currentState), ("fileImporter", fileImporter), ("openFileDialog", openFileDialog), ("leftImage", leftImage), ("leftLineList", leftLineList), ("rightImage", rightImage), ("currentLine", currentLine), ("rightLineList", rightLineList), ("mousePressed", mousePressed), ("currentCursorPosition", currentCursorPosition), ("previousCursorPosition", previousCursorPosition), ("cursorPositions", cursorPositions), ("rightGraph", rightGraph), ("isFilledMatrix", isFilledMatrix), ("linesMatrix", linesMatrix), ("deletionRadius", deletionRadius), ("historyOfActions", historyOfActions), ("overlayItems", overlayItems), ("redrawAss", redrawAss), ("markerRadius", markerRadius) }; var varArr = new List>(); foreach((String, object) obj in objArr) { varArr.Add(new Tuple(obj.Item1, obj.Item2)); } return varArr; } /// /// public method wrapper for testing purposes, invoking DrawEmptyCanvas(...) and BindAndDrawLeftImage(...) /// /// width of the parsed image /// height of the parsed image /// the parsed image public void CreateCanvasAndSetPictureForTesting(int width, int height, List newImage) { DrawEmptyCanvasLeft(width, height); BindAndDrawLeftImage(newImage); } } }