Monday, April 18, 2022

29-Component Delete

Overview

Deleting a component involves deleting all references which includes references in the component list and wire lists. For example, lets assume that we have simple circuit consisting of input port, resistor, output port, wire connecting input port to resistor and wire connecting resistor to output port.


We want to delete the resistor component. It is found in three places:

  • Pin wire list has a wire with the resistor as the output component.
  • The resistor is an item in the circuit component list.
  • The resistor has a wire with itself as the input component.
Deleting a component will also delete all connected wires. If we remove the resistor from the circuit component list, the output wire will be removed also. We will need to remove the input wire from the input port wire list.

To delete a wire, it is found in one place:
  • The input component wire list
For example, the wire from the input port to the resistor is found in the input port wire list. The wire from the resistor to the output port, is found in the resistor wire list. Remember that a component wire list contains a list of output wires.

From a user interface standpoint, it would be nice to "select" a component or wire, hit the delete key, ask the user to confirm that the selected component should be deleted, if confirmed, remove it and redraw the canvas.

To test component delete, we will reuse much of the TestRotation project source code as the starting point such that we test rotation and delete. Create a new project called TestDelete using C# WinForms and set as the startup project. Rename form1.cs to TestDeleteMainForm.cs.

UI Setup

Set the form properties:
  • Text: Test Delete
  • BackColor: PowerderBlue
  • KeyPreview: True
Add a flow layout panel to the form:
  • Dock: Top
  • BackColor: Black
  • Height: 44
Add a button to the panel:
  • Text: Init Ckt
  • Name: btnInit
  • Size: 82, 35
Copy the Init Ckt button and paste into the panel
  • Text: Select Mode
  • Name: btnSelect
  • Size: 126, 35
Add a pictureBox to the form:
  • Name: schematicCanvas

Class Organization

Add 5 folders in the Solution Explorer for class organization of the project:
  • Circuits
  • Components
    • Ideal
    • Lumped
  • Wires
Add 6 new classes to the folders:
  • Circuits: Circuit.cs class
  • Components: Comp.cs class
    • Ideal: InPort.cs class
    • Ideal: OutPort.cs class
    • Lumped: RES.cs class
  • Wires: Wire.cs class

Circuit Class

The component list will be moved from the MainForm in TestRotation to the Circuit class:

// C# Libraries
using System;
using System.Collections.Generic;
 
// Microwave Tools Libraries
using TestDelete.Components;
 
namespace TestDelete.Circuits
{
    public class Circuit
    {
        public List<Comp> comps = new List<Comp>();
    }
}

Comp.cs Base Class


Copy the attributes from TestRotation Comp.cs file and add the following parameters

       // Protected variables - available to derived subclasses
        protected int compSize = 60;
        protected int halfCompSize = 30;
        protected int leadL = 10;  // Length of input and output leads
        protected int bodyL = 40;  // Length of the component body (without leads)
        protected int compL = 60;  // Length of the component with leads
 
        // public variables
        public List<Wire> wires = new List<Wire>();
        public Point Pin;
        public Point Pout;
 
        // End caps variables
        protected const int endcap_radius = 3;
        protected bool endcapsVisible = false;
 
        // Selection flag
        public bool isSelected = false;

These additional attributes will be used to draw the components, create an output wire list, define the Pin and Pout locations, create end caps (small red rectangles), and provide a flag for when the component is selected.

Add a default constructor and the Draw() virtual method.

       // Constructors
        public Comp()
        {
 
        }
 
        // Polymorphism: Virtual methods used in circuit component iteration
        public virtual void Draw(Graphics gr) { /* Do nothing */ }

Add two new helper methods called checkSelect() and drawSelectRect(). The checkSelect() method will change the component drawPen to Red when it is selected. The drawSelectRect() method will add end caps to the leads at the point p1.

       public void checkSelect()
        {
            if (isSelected)
                drawPen = new Pen(Color.Red);
            else
                drawPen = new Pen(Color.Black);
        }
 
        public void drawSelectRect(Graphics gr, Point p1)
        {
            if (isSelected)
            {
                Rectangle rect1 = new Rectangle(
                     p1.X - endcap_radius, p1.Y - endcap_radius,
                     2 * endcap_radius, 2 * endcap_radius);
                gr.DrawRectangle(Pens.Red, rect1);     // Rectangular end cap
            }
        }

Input Port Class (InPort.cs)

// C# Libraries
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
 
namespace TestDelete.Components.Ideal
{
    public class InPort : Comp
    {
        public InPort()
        {
 
        }
 
        public InPort(double value, Point pt)
        {
            Loc.X = pt.X;
            Loc.Y = pt.Y;
            Width = 60;
            Height = 60;
            Value = value;
            boundBox = new Rectangle(Loc.X, Loc.Y, Width, Height);
 
            Pout = new Point(Loc.X + compSize, Loc.Y + halfCompSize);
        }

This class constructor initializes the input port location, width, height, value, bounding box and efines the location of the output pin (Pout).

        // Let the InPort draw itself called from the canvas paint event
        public override void Draw(Graphics gr)
        {
            // Component selection
            checkSelect();
            drawSelectRect(gr, new Point(Loc.X + compSize, Loc.Y+30));
 
            GraphicsPath gp = new GraphicsPath();
 
            Point p1 = new Point(Loc.X + compSize, Loc.Y + halfCompSize);             // Assume p1 is the end of the lead at the output of Pin
            Point p2 = new Point(p1.X - leadL, p1.Y);
            Point p3 = new Point(p2.X - 10, p2.Y - 10);
            Point p4 = new Point(p3.X - 30, p3.Y);
            Point p5 = new Point(p4.X, p4.Y + 20);
            Point p6 = new Point(p5.X + 30, p5.Y);
 
            gp.AddLine(p1, p2);
            gp.AddLine(p2, p3);
            gp.AddLine(p3, p4);
            gp.AddLine(p4, p5);
            gp.AddLine(p5, p6);
 
            gp.AddLine(p6, p2);
 
            // Draw the component text
            compText = "Pin";
            pt = new Point(Loc.X+10, Loc.Y+5);
            gp.AddString(compText, family, fontStyle, emSize, pt, format);
 
            // 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(drawPen, gp);
 
            // Draw the bounding box for debug
            //gr.DrawRectangle(redPen, boundBox);
 
            gp.Dispose();
        }
    }
}

 The input port Draw() method is very similar to the Draw() method used on TestComp class in the TestRotation project. Here we check to see if the component is selected.  A graphics path is created and all elements of the input port graphical symbol and text string are added. Next rotation is checked and the rotation angle set. The component is rotated and transformed. The bounding box is updated for the hit tests. The path is drawn on the graphics window and the graphics path is destroyed.

Resistor Class (RES.cs)

// C# Libraries
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
 
namespace TestDelete.Components.Lumped
{
    public class RES : Comp
    {
        public RES()
        {
 
        }
 
        public RES(double value, Point pt)
        {
            Loc.X = pt.X;
            Loc.Y = pt.Y;
            Width = 60;
            Height = 60;
            Value = value;
            boundBox = new Rectangle(Loc.X, Loc.Y, Width, Height);
 
            Pin = new Point(Loc.X, Loc.Y + 30);
            Pout = new Point(Loc.X + compSize, Loc.Y + 30);
        }
 
        public override void Draw(Graphics gr)
        {
            // Component selection
            checkSelect();
            drawSelectRect(gr, new Point(Loc.X, Loc.Y + 30));
            drawSelectRect(gr, new Point(Loc.X + compL - endcap_radius, Loc.Y + 30));
 
            GraphicsPath gp = new GraphicsPath();
 
            // Draw the input leads
            gp.AddLine(Loc.X, Loc.Y+30, Loc.X + leadL, Loc.Y + 30);
 
            // Draw the resistor body
            for (int i = 1; i < 5; i++)
            {
                gp.AddLine(Loc.X + leadL * (i), Loc.Y + 30, Loc.X + leadL * (i) + 3, Loc.Y - leadL + 30);
                gp.AddLine(Loc.X + leadL * (i) + 3, Loc.Y - leadL + 30, Loc.X + leadL * (i) + 6, Loc.Y + leadL + 30);
                gp.AddLine(Loc.X + leadL * (i) + 6, Loc.Y + leadL + 30, Loc.X + leadL * (i + 1), Loc.Y + 30);
            }
 
            // Draw the output leads
            gp.AddLine(Loc.X + bodyL + leadL, Loc.Y + 30, Loc.X + compL, 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);
 
            // 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(drawPen, gp);
 
            // Draw the bounding box for debug
            //gr.DrawRectangle(redPen, boundBox);
 
            gp.Dispose();
        }
    }
}

Output Port Class (OutPort.cs)

// C# Libraries
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
 
namespace TestDelete.Components.Ideal
{
    public class OutPort : Comp
    {
        public OutPort()
        {
 
        }
 
        public OutPort(double value, Point pt)
        {
            Loc.X = pt.X;
            Loc.Y = pt.Y;
            Width = 60;
            Height = 60;
            Value = value;
            boundBox = new Rectangle(Loc.X, Loc.Y, Width, Height);
 
            Pin = new Point(Loc.X, Loc.Y + 30);
        }
 
        //Let the InPort draw itself called from the canvas paint event
        public override void Draw(Graphics gr)
        {
            checkSelect();
            drawSelectRect(gr, new Point(Loc.X, Loc.Y + 30));
 
            GraphicsPath gp = new GraphicsPath();
 
            Point p1 = new Point(Loc.X, Loc.Y+30);     // Assume p1 is the end of the lead at the output of Pin
            Point p2 = new Point(p1.X + leadL, p1.Y);
            Point p3 = new Point(p2.X + 10, p2.Y - 10);
            Point p4 = new Point(p3.X + 30, p3.Y);
            Point p5 = new Point(p4.X, p4.Y + 20);
            Point p6 = new Point(p5.X - 30, p5.Y);
 
            gp.AddLine(p1, p2);
            gp.AddLine(p2, p3);
            gp.AddLine(p3, p4);
            gp.AddLine(p4, p5);
            gp.AddLine(p5, p6);
            gp.AddLine(p6, p2);
 
            // Draw the component text
            compText = "Pout";
            pt = new Point(Loc.X + 20, Loc.Y + 5);
            gp.AddString(compText, family, fontStyle, emSize, pt, format);
 
            // 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(drawPen, gp);
 
            // Draw the bounding box for debug
            //gr.DrawRectangle(redPen, boundBox);
 
            gp.Dispose();
        }
    }
}

Wire Class (Wire.cs)

// C# Libraries
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
 
// Microwave Tools Libraries
using TestDelete.Components;
 
namespace TestDelete.Wires
{
    public class Wire : Comp
    {
        // Create wire start and end points
        public Point Pt1 = new Point();
        public Point Pt2 = new Point();
 
        // Add components connected to the input and output of the wire
        public Comp Cin = new Comp();
        public Comp Cout = new Comp();
 
        public Wire()
        {
 
        }
 
        public Wire(Comp cin, Comp cout)
        {
            Cin = cin;
            Cout = cout;
 
            Cin.wires.Add(this);
 
            Pt1 = new Point(cin.Pout.X, cin.Pout.Y);
            Pt2 = new Point(cout.Pin.X, cin.Pout.Y);
 
            Loc = Pt1;
 
            Width = Math.Abs(Pt2.X - Pt1.X);
            Height = Math.Abs(Pt2.Y - Pt1.Y);
            if (Height == 0)
                Height = 10;
 
            boundBox = new Rectangle(Pt1.X, Pt1.Y, Width, Height);
        }
 
        public override void Draw(Graphics gr)
        {
            // Draw the wire end caps
            drawEndCaps(gr);
            checkSelect();
 
            if (Pt1.X != Pt2.X || Pt1.Y != Pt2.Y)
            {
                // Draw L-shaped wire
                gr.DrawLine(drawPen, Pt1.X, Pt1.Y, Pt1.X, Pt2.Y); // Vertical line
                gr.DrawLine(drawPen, Pt1.X, Pt2.Y, Pt2.X, Pt2.Y); // Horizontal line
            }
            else
            {
                // Draw straight wire
                gr.DrawLine(drawPen, Pt1, Pt2);
            }
        }
 
        private void drawEndCaps(Graphics gr)
        {
            if (this.endcapsVisible || this.isSelected)
            {
                // Draw custom end cap for Pt1
                Rectangle rect1 = new Rectangle(
                     Pt1.X - endcap_radius, Pt1.Y - endcap_radius,
                     2 * endcap_radius, 2 * endcap_radius);
                gr.DrawRectangle(Pens.Red, rect1);     // Rectangular end cap
 
                // Draw custom end cap for Pt2
                Rectangle rect2 = new Rectangle(
                     Pt2.X - endcap_radius, Pt2.Y - endcap_radius,
                     2 * endcap_radius, 2 * endcap_radius);
                gr.DrawRectangle(Pens.Red, rect2);    // Rectangular end cap
            }
        }
    }
}
 

Note that the wire Draw() method draws the wire directly to the graphics window.

Select Component

Add a new button on the MainForm:
  • Text: Select Mode
  • Name: btnSelect
Add a boolean attribute to MainForm.cs:

       private bool isSelectMode = false;

Add a click event handler to btnSelect to toggle the logic state of isSelectMode:

       private void btnSelect_Click(object sender, EventArgs e)
        {
            isSelectMode = !isSelectMode;
        }

When the isSelectMode flag is set, the user can select a component or wire on the schematic.

In MainForm.cs add a MouseDown event handler and hitTest to check if the user has clicked on a component. If a component is hit, set the components isSelected flag to true.

        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 ckt.comps)
                {
                    if (hitTest(comp))
                    {
                        tempComp = comp;
                        offset.X = NewPt1.X - comp.Loc.X;
                        offset.Y = NewPt1.Y - comp.Loc.Y;
 
                        if (isSelectMode)
                        {
                            if (!comp.isSelected)
                                comp.isSelected = true;
                            else
                                comp.isSelected = false;
                        }
 
                        schematicCanvas.Invalidate();
                    }
 
                    foreach(Wire wire in comp.wires)
                    {
                        if (isSelectMode)
                        {
                            if (hitTest(wire))
                            {
                                if (!wire.isSelected)
                                    wire.isSelected = true;
                                else
                                    wire.isSelected = false;
                            }
                        }
 
                        schematicCanvas.Invalidate();
                    }
                }
            }
        }

Mouse down iterates over all components in the component list as well as all wires in each wire list and sets the isSelected flag.

       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;
        }

In Comp.cs, add two helper methods that change the drawPen to Red if the component is selected and draw red rectangles (end caps) on the input and/or output leads.

        public void checkSelect()
        {
            if (this.isSelected)
                this.drawPen = new Pen(Color.Red);
            else
                this.drawPen = new Pen(Color.Black);
        }
 
        public void drawSelectRect(Graphics gr, Point p1)
        {
            if (this.isSelected)
            {
                Rectangle rect1 = new Rectangle(
                     p1.X - endcap_radius, p1.Y - endcap_radius,
                     2 * endcap_radius, 2 * endcap_radius);
                gr.DrawRectangle(Pens.Red, rect1);     // Rectangular end cap
            }
        }

Each of the component subclasses have calls to checkSelect() and drawSelectRect() in the Draw() method. Note that a call to drawSelectRect() is needed for each input/output lead.

Run the program and verify that you can select a component with the mouse.


Delete Component

In MainForm.cs, add a KeyDown event handler to detect the Delete keypress:

        private void TestDeleteMainForm_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Delete)    // 46 - Delete Key
            {
                confirmDelete();
            }
        }

Note that we used the KeyDown event rather than the KeyPress event which allows the use of KeyCodes and KeyPress will not detect the Delete key.

Next, add the confirmDelete() method to verify that the currently selected component should be deleted.

        void confirmDelete()
        {
            DialogResult res = MessageBox.Show("Are you sure you want to Delete all selected                 components",
                "Confirmation", MessageBoxButtons.OKCancel, MessageBoxIcon.Information);
            if (res == DialogResult.OK)
            {
                Debug.WriteLine("Delete confirmed!");
                deleteSelectedComps();
            }
            if (res == DialogResult.Cancel)
            {
                Debug.WriteLine("Delete cancelled...");
            }
        }

In order to show the MessageBox, schematicCanvas.Invalidate() not be included in the Paint event as it will keep invalidating the window. 

        private void schematicCanvas_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
 
            foreach (Comp comp in ckt.comps)
            {
                comp.Draw(e.Graphics);
 
                foreach (Wire wire in comp.wires)
                {
                    wire.Draw(e.Graphics);
                }
            }
        }

Finally, add a deleteSelectedComps() method that will delete all references to the selected component. This will be done in "for" loops rather than "foreach" using array indexes to the component and wire lists.

        private void deleteSelectedComps()
        {
            // Search for selected components
            for (int i = 0; i < ckt.comps.Count; i++)
            {
                if (ckt.comps[i].isSelected)
                {
                    // Iterate over all components again
                    for (int k = 0; k < ckt.comps.Count; k++)
                    {
                        // Iterate over the wire list for the current component
                        for (int j = 0; j < ckt.comps[k].wires.Count; j++)
                        {
                            // If wire Cout = selected component, delete the wire
                            if (ckt.comps[k].wires[j].Cout == ckt.comps[i])
                                ckt.comps[k].wires.RemoveAt(j);
                        }
                    }
 
                    // Delete the selected component
                    ckt.comps.RemoveAt(i);
                }
            }
 
            schematicCanvas.Invalidate();
        }


We have successfully integrated component rotation and delete capabilities into the TestDelete project. The full source code for the Test Delete project 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...