Saturday, April 16, 2022

28-Component Rotation

In this post, we will investigate the rotation of components using the "R" key on the keyboard. Currently, the Draw() method for each component draws the graphics symbol with no rotation. We could create a new graphics symbol for each rotation angle but that seems tedious, there must be a better way. Indeed, C# has a Graphics Path class which allows 2D transformations such as translation and rotation. Note that the path consists of lines, rectangles, arcs, text, etc.

To test rotation on a Graphics Path, create a new project called TestRotation using WinForms. Rename form1.cs to TestRotationMainForm.cs.

Test Rotation Main Form

Set TestRotationMainForm properties to:

  • Text: Test Rotation
  • StartPosition: CenterScreen
Add a pictureBox to the form and set properties to:
  • Name: schematicCanvas
  • BackColor: Beige

Component Base Class

Add a new class to the project called Comp.cs and set it to public. This class be the base class for the component class. Add the following attributes which includes a new "angle" variable to keep track of the rotation angle in degrees.

    public class Comp
    {
        // Component location, rotation, and size variables
        public Point Loc;
        public double Value;
        public float angle = 0.0F;
        public int Width, Height;
      
        . . .
    }

The next variable will be a rectangle called boundBox which rotates with the component virtually and is used by the hit test function. If the mouse clicks within the bounding box for a component, it is hit.

        // Component bounding box for hit test
        public Rectangle boundBox = new Rectangle();

Add two pens for normal drawing and a red pen for displaying the bounding box in debug mode.

        // Component pens
        protected Pen drawPen = new Pen(Color.Black, 1);
        protected Pen redPen = new Pen(Color.Red, 1);

The addString() method will be used on the graphics path and requires the following parameters and arguments. The first four variables setup the string font, style, size, and format. The next two variables are the component text and it's location point.

        // Component String variables
        protected FontFamily family = new FontFamily("Arial");
        protected int fontStyle = (int)FontStyle.Regular;
        protected int emSize = 12;
        protected StringFormat format = StringFormat.GenericDefault;
 
        // Component text variables
        protected String compText;
        protected Point pt;

Finally, add a default constructor and the virtual Draw() function that is overriden by subclasses.

        public Comp()
        {
 
        }
 
        public virtual void Draw(Graphics gr) { /* Do nothing */ }

Test Component Class

Rotation of components will require some architectural changes to component Draw() methods.  The graphics path variable is a "container" for other graphical items such as lines and rectangles. The location point is the point in screen coordinates where the symbol will be drawn. The angle variable stores the rotation angle for the component in degrees.

Create a new class called TestComp.cs and set it to public access and inherit the Comp base class.

Add the following C# libraries:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace TestRotation
{
    public class TestComp : Comp
    {

    }

Next, we will initialize the component in the class constructor with a fixed location, width, height, value, and bounding box.

       public TestComp()
        {
            Loc.X = 300;
            Loc.Y = 200;
            Width = 60;
            Height = 60;
            Value = 75.0;
            boundBox = new Rectangle(Loc.X, Loc.Y, Width, Height);
        }

Create a Draw() method that overrides the base class virtual method. Note that the component symbol must be redrawn in the Draw() method based on its location during mouse moves and component rotation. In the Draw() method we add the elements of the graphics symbol to a graphics path. This test will draw the MLIN symbol.

       public override void Draw(Graphics gr)
        {
            GraphicsPath gp = new GraphicsPath();
 
            gp.AddLine(Loc.X, Loc.Y + 30, Loc.X + 10, Loc.Y + 30);
            gp.AddRectangle(new Rectangle(Loc.X + 10, Loc.Y + 20, 40, 20));
            gp.AddLine(Loc.X + 50, Loc.Y + 30, Loc.X + 60, Loc.Y + 30);
 
            // Draw the component text
            compText = "R = " + this.Value + "Ω";
            pt = new Point(Loc.X + 5, Loc.Y + 5);
            gp.AddString(compText, family, fontStyle, emSize, pt, format);
 
            // Draw the component path
            gr.DrawPath(Pens.Black, gp);
 
            gp.Dispose();
        }

We define a GraphicsPath variable called gp. Then we add lines, rectangles, and the component text string to the path. The path is draw on the graphics window using the gr.DrawPath() function. Finally, the path is disposed of.

Testing Graphics Path based Component

Let's enable the elements of the MainForm to allow us to test a component drawn with a graphics path. In TestRotationMainForm.cs add the following variables:

namespace TestRotation
{
    public partial class TestRotationMainForm : Form
    {
        // The grid spacing.
        public const int grid_gap = 10;
 
        List<Comp> comps = new List<Comp>();
        TestComp comp = new TestComp();
 
        private Point NewPt1, NewPt2, offset;
        Comp tempComp = new Comp();
 
        // Mouse event variables
        bool isMouseDown = false;
        bool lineDrawing = false;

grid_gap defines the grid spacing. We declare a list of components called comps to represent the list of elements in the circuit or schematic. We declare a TestComp object called comp which is the test component created in the subclass above. We define various points and offsets used in the mouse event handlers. We define a temporary component called tempComp (not to be confused with TestComp). Then, two flags are created that indicate that a mouse event has started and if a line is being drawn.

In order to test the new component rotation capability, it is a good idea to setup the form with grid, mouse event, paint event, resize event, and helper functions for the grid and hit test. We will evaluate the rotation for component movement, component text, and snap to grid. We will copy and modify the events from the TestDiagram project.

Add the DrawBackgroundGrid() and SnapToGrid() functions from TestDiagram to TestDeleteMainForm.cs with no changes.

        private void DrawBackgroundGrid()
        {
            Bitmap bm = new Bitmap(
                schematicCanvas.ClientSize.Width,
                schematicCanvas.ClientSize.Height);
            for (int x = 0; x < schematicCanvas.ClientSize.Width; x += grid_gap)
            {
                for (int y = 0; y < schematicCanvas.ClientSize.Height; y += grid_gap)
                {
                    bm.SetPixel(x, y, Color.Black);
                }
            }
 
            schematicCanvas.BackgroundImage = bm;
        }
 
        private void SnapToGrid(ref int x, ref int y)
        {
            //if (!chkSnapToGrid.Checked) return;
            x = grid_gap * (int)Math.Round((double)x / grid_gap);
            y = grid_gap * (int)Math.Round((double)y / grid_gap);
        }

This will test that the rotated component will snap to grid.

Next, add the schematic canvas resize event. Again no changes.

        private void schematicCanvas_Resize(object sender, EventArgs e)
        {
            DrawBackgroundGrid();
        }

Add the schematic canvas paint event to call the Draw() method on all components in the component list.

        private void schematicCanvas_Paint(object sender, PaintEventArgs e)
        {
            foreach (Comp comp in comps)
            {
                comp.Draw(e.Graphics);
            }
        }

Next add the HitTest() method which has been refactored to use the component boundBox to detect if the mouse has hit a component.

       private bool hitTest(Comp comp)
        {
            Debug.WriteLine("Hit test on comp: " + comp.ToString() + " Loc: " + comp.Loc.ToString());
            Debug.WriteLine("Mouse Pos: " + NewPt1.ToString());
            bool hit;
 
            hit = false;
            if ((NewPt1.X >= comp.boundBox.X && NewPt1.X <= comp.boundBox.X + comp.boundBox.Width) &&
                (NewPt1.Y >= comp.boundBox.Y && NewPt1.Y <= comp.boundBox.Y + comp.boundBox.Height))
            {
                Debug.WriteLine("Hit!");
                hit = true;
            }
            else
            {
                Debug.WriteLine("No Hit!");
                hit = false;
            }
 
            return hit;
        }

Note that the width and height of the test component is 60 x 60.

Next, add the mouse down, mouse move, and mouse up events to the schematicCanvas without the line drawing code.


       private void schematicCanvas_MouseDown(object sender, MouseEventArgs e)
        {
            isMouseDown = true;
 
            // Snap the start point to the Grid
            int x = e.X;
            int y = e.Y;
            SnapToGrid(ref x, ref y);
            NewPt1.X = x;
            NewPt1.Y = y;
 
            if (!lineDrawing)
            {
                // Iterate over the component list and test each to see if it is "hit"
                foreach (Comp comp in comps)
                {
                    if (hitTest(comp))
                    {
                        tempComp = comp;
                        offset.X = NewPt1.X - comp.Loc.X;
                        offset.Y = NewPt1.Y - comp.Loc.Y;
                    }
                }
            }
        }

        private void schematicCanvas_MouseMove(object sender, MouseEventArgs e)
        {
            if (isMouseDown == true)
            {
                int x = e.X;
                int y = e.Y;
                SnapToGrid(ref x, ref y);
 
                if (!lineDrawing)
                {
                    if (tempComp != null)
                    {
                        tempComp.Loc = new Point(x - offset.X, y - offset.Y);
                    }
                }
 
                schematicCanvas.Invalidate(); // Refresh the drawing canvas pictureBox
            }
        }
 
        private void schematicCanvas_MouseUp(object sender, MouseEventArgs e)
        {
            isMouseDown = false;
            tempComp = null;
        }

If you run the program now, the graphics symbol will be drawn with its associated text. It can be moved with the mouse. So far, so good.

Testing Rotation

We will use a Form KeyPress event to detect that the user has pressed the "R" key to rotate the test component. Add the KeyPress event to the MainForm:

        private void Form1_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 114 || e.KeyChar == 82)    // 114 = r, 82 = R
            {
                comp.isRotated = !comp.isRotated;
                schematicCanvas.Invalidate();
            }
        }

Now, modify the TestComp.cs code to detect that the component is rotated, rotate it about its center point, and draw the bounding box on the screen.


        public override void Draw(Graphics gr)
        {
            GraphicsPath gp = new GraphicsPath();
 
     . . .
 
            // Set rotation angle
            if (isRotated)
                angle = 90;
            else
                angle = 0;
 
            // Rotate the component by angle deg
            PointF rotatePoint = new PointF(Loc.X + 30, Loc.Y + 30); // Rotate about component center point
            Matrix myMatrix = new Matrix();
            myMatrix.RotateAt(angle, rotatePoint, MatrixOrder.Append);
            gr.Transform = myMatrix;
 
            // Update the bounding box location
            boundBox = new Rectangle(Loc.X, Loc.Y, Width, Height);
           
            // Draw the component path
            gr.DrawPath(Pens.Black, gp);
 
            // Draw the bounding box for debug
            gr.DrawRectangle(redPen, boundBox);
 
            gp.Dispose();
        }

This code sets the rotation angle to one of two angles: 90 deg or 0 deg. The component is rotated at its center point. The matrix is used to determine the rotation transorm and applied to the graphics gr. Next, the bounding box is updated, the path is drawn, and the bouding box is drawn for debug.

If you run application now, the component is drawn with a bounding box. Press the "R" key to rotate the component and note that it can be moved as usual. The source code for this post is available on GitHub.





No comments:

Post a Comment

34-Microwave Tools with Analysis (Series Final Post)

In this final blog post, I have integrated Y-Matrix analysis into the Microwave Tools project. This version has addition Lumped, Ideal, Micr...