Recently I decided to write an algorithm that draws the path of a chain (like a bike chain) as it runs around a set of cogs. Cogs can have any position and radius. However, they must be listed in clockwise order. I've just got it working and i want to post some pictures for your delectation.
I'll post the code at some point (VB.Net).
8 cogs, 3 of them anticlockwise rotation (detected automatically by the alg)...
OK, solved the artefacts by using 2 paths instead of one.
Note to hyper geeks: look closely at the bend in the centre of the image. you'll see that one section of white kerb is longer than the others. That is where the 2 ends of the curve path meet each other and overlap. I'm working on a way to eliminate this buts only a geek would notice it
192.168.0.1 Preferred Animal: Penguin Reason for errors: Line#38
Posts
3,051
Re: Gallery - Drawing tangents to circles
Very nice stuff!!
Is the width of the track done using 2 paths ment to vary?
Quotes:
"I am getting better then you guys.." NoteMe, on his leet english skills.
"And I am going to meat her again later on tonight." NoteMe "I think you should change your name to QuoteMe" Shaggy Hiker, regarding NoteMe
"my sweet lord jesus. I've decided never to have breast implants" Tom Gibbons
Hi Woss, I suspect that Dave is preoccupied with his latest bundle of joy so maybe I can step in with my own "algorithm". I'll assume you want to create a track from any arrangement of corners, not just those you have illustrated; and you work around the track clockwise.
1. Let's suppose each corner of the track is represented by a point P, an inner radius r and an outer radius R.
2. The points P can be connected up as a polygon which has convex and concave vertices. Get the convexity of each vertex (see code below).
3. Start building the outer graphicspath with the outer radius R1 of the first vertex. Then for each vertex Pn:
4. If the next vertex Pn+1 has the same convexity as Pn you need to find the outer tangent (one which does not cross the line between the centres) for the same radius (inner or outer). So if you are creating the outer path for Pn (Rn), you need the tangent to the outer radius of Pn+1 (Pn+1R). To find the external tangent from PnR to Pn+1R:
a. Get the slope mn between the centres, i.e. Pn-Pn+1, using Math.Atan2.
b. Get the distance Dn between the centres Pn - Pn+1 (by Pythagorus).
c. Find the difference between the radii e.g. diffRn = Pn+1R - PnR. The result can be positive or negative.
d. Get the relative tangent slope, which (I think) will be mRn = Math.ASin(Dn/diffRn). This can also be positive or negative.
e. Subtract the relative slope from the slope between centres: mTn =mn - mRn. This is the actual slope of the tangent. Convert it from radians to degrees and save it.
5. If the next vertex Pn+1 has opposite convexity to Pn, then you need to find the cross tangent (which passes through the line between the circle centres) to the opposite radius (outer or inner). For example, if you are creating the outer path from PnR and Pn is convex, you need the cross-tangent to Pn+1r.
a, b. Get the slope between centres mn and the distance Dn as in 4.
c. Find the sum of the radii sumn = Rn + rn+1 or rn + Rn+1 depending on whether you are going from convex to concave or vice-versa. Make it positive if you are going from convex to concave, and negative from concave to convex.
d. Get the relative tangent slope of the cross tangent nn = Math.Asin(Dn/sumn). This will be correspondingly positive or negative.
e. Add the relative slope to (5a) to get the tangent slope: mTn = mn + nn.
6. If you've survived this far, you have list of tangent angles for each vertex of the outer path. Now you need to find the arc around each vertex and add it to the path.
7. To get the start angle for the arc for vertex Pn, get the tangent slope mTn-1 for the previous vertex. Subtract 90 degrees to get the normal to the tangent.
8. To get the sweep angle for the arc of vertex Pn, get the tangent slope mTn and subtract 90 degrees. Then subtract the start angle. If Pn is concave, make the result negative.
9. Add the arc to the path. There is no need to add the tangents themselves to the path. Just add the arcs and use GraphicsPath.CloseFigure.
10. To get the inner path, repeat all of the above starting from the inner radius of P1. Maybe a few things will have to be the other way round. I'm exhausted already.
Oh yes, here's my simple code for getting the convexity of a polygon vertex. It just removes the vertex from the polygon, then checks if the vertex is inside or outside the resulting polygon. It works reliably for non self-intersecting polygons.
Code:
Private Function VertexIsConcave(polygon As PointF(), vertex As PointF) As Boolean
Dim vertices As New List(Of PointF)(polygon)
vertices.Remove(vertex)
Dim gp As New Drawing2D.GraphicsPath
gp.AddPolygon(vertices.ToArray)
Return gp.IsVisible(vertex)
End Function
Yes, I think you've formally stated in mathematical terms all the stuff that was floating around in my brain while I was writing my old code.
I remember it well. To be honest, I have no idea what is the current state of the art as far as non-accelerated graphics in .Net is. Last time I did this I was hacking around with Brush, Pen, GraphicsPaths and Lockbits and huge amounts of "unmanaged" pointer math code in order to get the framerate up. I have not programmed C# seriously in any IDE more recent than VS2005 so I can only be of limited assistance.
Looks like you've got a good handle on it though. I eagerly await your first screenshots
Heh, me too. Luckily there's not too much math involved, the trigonometry is little more than atan2() and a bit of logic to decide if 3 consecutive points go clockwise or anticlockwise. The hardest part was actually fixing the weird .Net graphical issues.
You should give it a shot too, it's good fun to mess with C# graphics in a simple 2D context like this. I learned a lot from this old project.
Yes, I think you've formally stated in mathematical terms all the stuff that was floating around in my brain while I was writing my old code.
I remember it well. To be honest, I have no idea what is the current state of the art as far as non-accelerated graphics in .Net is. Last time I did this I was hacking around with Brush, Pen, GraphicsPaths and Lockbits and huge amounts of "unmanaged" pointer math code in order to get the framerate up. I have not programmed C# seriously in any IDE more recent than VS2005 so I can only be of limited assistance.
Looks like you've got a good handle on it though. I eagerly await your first screenshots
I'll see if I can work something out in the weekend (see also my PM). I'm sure GDI+ will be fast enough for designing the track. Getting the cars to race around it convincingly may be another story.
Edit: @dave. Once the track is designed, I assume you could port the design as a texture, together with the path boundaries as polygons, to XNA. I would happily leave that to an XNAxpert.
BB
Last edited by boops boops; Jan 30th, 2014 at 06:49 PM.
Sounds simple enough, I actually have a method that converts bitmaps to Texture2ds which is what this would entail. If I have the track, I could certainly build something that would make cars move around that track.
Let those who thought I couldn't do it weep. Here's a pic of how the race track looks in Edit mode. The light gray dots are the corner centres, which can be dragged to any position. The yellow dot is the selected dot. The dashed circles around it are the corner's inner and outer radii, which you can adjust with the mouse wheel. These details are hidden in Play mode. The dashed kerb was no problem whatsoever (Pen.DashPattern) but the logic of the tangent angles and arcs kept me up all night. There's a few minor bugs (aren't there always) still to sort out and a few features still to implement (adding/deleting corners in Edit mode).
Last edited by boops boops; Feb 5th, 2014 at 07:43 PM.
Reason: image reattached. I don't know why it went away.
Alright boops boops, I've tried my hand at following your math, but I must say that I'm just ignorant when it comes to math... Here are my classes:
Code:
Option Strict On
Option Explicit On
Public Class Cog
#Region "Properties"
Private pt As PointF
<System.ComponentModel.Description("Gets/Sets the center point of the cog.")> _
Public Property Location() As PointF
Get
Return pt
End Get
Set(ByVal value As PointF)
pt = value
End Set
End Property
Private iRadius As Single
<System.ComponentModel.Description("Gets/Sets the distance in pixels from the Location to the inner radius of the cog.")> _
Public Property InnerRadius() As Single
Get
Return iRadius
End Get
Set(ByVal value As Single)
iRadius = value
End Set
End Property
Private oRadius As Single
<System.ComponentModel.Description("Gets/Sets the distance in pixels from the Location to the outer radius of the cog.")> _
Public Property OuterRadius() As Single
Get
Return oRadius
End Get
Set(ByVal value As Single)
oRadius = value
End Set
End Property
#End Region
#Region "New Constructors"
Sub New()
pt = New Point(0, 0)
iRadius = 1
oRadius = 2
End Sub
Sub New(ByVal inner As Single, ByVal outer As Single)
iRadius = inner
oRadius = outer
End Sub
#End Region
End Class
And...
Code:
Option Strict On
Option Explicit On
Public Class Track
Inherits System.Windows.Forms.Control
#Region "Properties"
Private cogCollection As Cog()
<System.ComponentModel.Description("Gets the collection of cogs that make up the track. NOTE: The order of the cogs plays a huge importance in the layout of the track.")> _
Public ReadOnly Property Cogs() As Cog()
Get
Return cogCollection
End Get
End Property
Private isDash As Boolean
<System.ComponentModel.Description("Gets/Sets if the outside border of the track is dashed.")> _
Public Property DashTrack() As Boolean
Get
Return isDash
End Get
Set(ByVal value As Boolean)
isDash = value
End Set
End Property
Private dashPrim As Color
<System.ComponentModel.Description("Gets/Sets the primary dash color of the track if the DashTrack property is True.")> _
Public Property PrimaryDashColor() As Color
Get
Return dashPrim
End Get
Set(ByVal value As Color)
dashPrim = value
End Set
End Property
Private dashSecond As Color
<System.ComponentModel.Description("Gets/Sets the secondary dash color of the track if the DashTrack property is True.")> _
Public Property SecondaryDashColor() As Color
Get
Return dashSecond
End Get
Set(ByVal value As Color)
dashSecond = value
End Set
End Property
#End Region
#Region "Methods"
'Public Methods
Public Sub Add(ByVal cog As Cog)
'Convert the cogCollection to a list and add the cog to that list
'Convert the list back to an array
Dim l As List(Of Cog) = cogCollection.ToList
l.Add(cog)
cogCollection = l.ToArray
End Sub
Public Sub AddRange(ByVal cogs() As Cog)
'Convert the cogCollection to a list and add the cogs to that list
'Convert the list back to an array
Dim l As List(Of Cog) = cogCollection.ToList
l.AddRange(cogs)
cogCollection = l.ToArray
End Sub
Public Sub ChangeOrder(ByVal priorIndex As Integer, ByVal newIndex As Integer)
'Convert the cogCollection to a list and get the cog at the prior index
'Insert the cog at the new index
'Convert the list back to an array
Dim l As List(Of Cog) = cogCollection.ToList
Dim c As Cog = l.Item(priorIndex)
l.Remove(c)
l.Insert(newIndex, c)
cogCollection = l.ToArray
End Sub
Public Sub Remove(ByVal cog As Cog)
'Convert the cogCollection to a list and remove the cog from that list
'Convert the list back to an array
Dim l As List(Of Cog) = cogCollection.ToList
l.Remove(cog)
cogCollection = l.ToArray
End Sub
Public Sub RemoveAt(ByVal index As Integer)
'Convert the cogCollection to a list and remove the cog by it's index
'Convert the list back to an array
Dim l As List(Of Cog) = cogCollection.ToList
l.RemoveAt(index)
cogCollection = l.ToArray
End Sub
'Private Methods
Private Function CreatePath() As Drawing2D.GraphicsPath
Dim path As List(Of PointF) = New List(Of PointF)
Dim oRadius As List(Of Drawing2D.GraphicsPath) = New List(Of Drawing2D.GraphicsPath)
Dim iRadius As List(Of Drawing2D.GraphicsPath) = New List(Of Drawing2D.GraphicsPath)
For Each c As Cog In cogCollection
'Get the inner and outer radius rectangle of the cog
Dim iRect As RectangleF = New RectangleF(New PointF(c.Location.X - c.InnerRadius, c.Location.Y - c.InnerRadius), New SizeF(c.InnerRadius * 2, c.InnerRadius * 2))
Dim oRect As RectangleF = New RectangleF(New PointF(c.Location.X - c.OuterRadius, c.Location.Y - c.OuterRadius), New SizeF(c.OuterRadius * 2, c.OuterRadius * 2))
'Add the inner radius of the cog to a graphics path for the inner rectangle
Dim iGraphics As Drawing2D.GraphicsPath = New Drawing2D.GraphicsPath
iGraphics.AddEllipse(iRect)
'Add the outer radius of the cog to a graphics path for the inner rectangle
Dim oGraphics As Drawing2D.GraphicsPath = New Drawing2D.GraphicsPath
oGraphics.AddEllipse(oRect)
'Add the inner and outer radius graphics path to their list
iRadius.Add(iGraphics)
oRadius.Add(oGraphics)
Next
'Add the outer radius of the first vertex
path.Add(oRadius.Item(0).PathPoints(0))
'Loop through each vertex
For i As Integer = 0 To oRadius.Item(0).PathPoints.Count - 1
Next
Return Nothing
End Function
'Thank you boops boops for this function
Private Function VertexIsConcave(ByVal polygon As PointF(), ByVal vertex As PointF) As Boolean
Dim vertices As List(Of PointF) = New List(Of PointF)(polygon)
vertices.Remove(vertex)
Dim gp As New Drawing2D.GraphicsPath
gp.AddPolygon(vertices.ToArray)
Return gp.IsVisible(vertex)
End Function
#End Region
#Region "Events"
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
End Sub
#End Region
End Class
Your step 3 is only part of the story as far as I am concerned. I frittered away many a frustrated hour trying to develop my initial attempt into a bug-free track designer with "a few extra features". Well, that was in January and February: by mid March I had an imaginary sign flashing in front of my screen every few minutes with the message "get a life!" Still, most of it is now working as intended. I'll send you a PM with a link to my code.
Here's a new pic illustrating features like overlapping and multiple paths:
A comment about your code. Wouldn't it be easier to declare CogCollection as a List(Of Cog)? Then you wouldn't need those conversion methods.