Xojo: About cables, chains and other curves…

Mimicking Propellerheads Reason interface in Xojo

Mimicking Propellerheads Reason interface in Xojo

When I first saw Propellerheads Reason interface years ago, I was mesmerised by the cabling system. It seemed so intuitive to connect devices with each other. I rebuilded such an interface in RealBasic and was very satisfied with it. Lataly, I stumbled on this old project and I decided to give it a go in Xojo. It seemed like a good idea for an article on my blog.

For this article I wrote some classes (ABCable mainly) so it’s easy to implement in other projects in Xojo. I wrote a quick and dirty system to add and move cables cutting a couple of corners, as you may know I tend to do 🙂
When you want to use such a system, I urge you to rewrite that part in a more stable framework. But all the principles are here.

The Windows and OSX versions will also look slightly different. I did not find a good way to draw a smooth Catenary curve with the MacOSLib so I decided to use different functions for them, simulating the Windows version as much as possible. (Check DrawOSX and DrawWIN in the ABCable class to see what I’ve done). You can rewrite them as you please.

ABCable has three possible constructors:

' use for itaerations
Constructor()
' the start and end point, the color, the minimum length of the cable (will not be exact), the cable thickness (experimental)
Constructor(iStartPoint as ABPoint, iEndPoint as ABPoint, iCableColor as Color, iCableMinimumLength as double, iCableThickness as double )
' is the same as above but with an additional parameter: iPointSpacing
' this param can be used to 'smoothen' the curve by considering more points. The default is 8 segments. Increasing this number will slow down the animation.
Constructor(iStartPoint as ABPoint, iEndPoint as ABPoint, iCableColor as Color, iCableMinimumLength as double, iPointSpacing as Double, iCableThickness as double )

The other functions you’ll need from the ABCable class are:

Draw(DrawType as integer)

This function updates the graphics. I added two types of drawing. You can use DrawType to change it to the way you like:
DRAWBEZIER: Draw the curve as a Bezier curve
DRAWCATENARY: Draw the curve as a Catenary curve (for OSX it’s a simulation as described above)

The left one is as a Bezier, the other as a Catenary.

Drawtype as Bezier and as Caterary

Drawtype As Bezier and as Caterary

SetStartPoint(value as ABPoint)
SetEndPoint(value as ABPoint)

With these functions you can change the the Start en End connection points of the cable.

All the rest is done automatically (the shadow of the cable, the length increases/decreases when needed, etc…)

Let’s get into the rest of the program using this class.

I’m using a trick we learned in a previous canvas lesson to find where cables are and if there are connection points. We need 3 pictures. Our background without cables, a picture with the possible connection points in yellow (&cFFFF00) and a third one with the current connections. This last one will be created within the program and changes dynamically. (I’ll go deeper into this last one later in this article)

Principle

Click to enlarge

I’ll go through some of the source code and explain each part as we encounter it.

First, setup manual double buffering as we did in all the other lessons (using pBuffer as Picture, gBuffer as Graphics) so that our paint event in the canvas can simply be:

Sub Paint(g As Graphics)
  if pBuffer <> nil then
    g.DrawPicture pBuffer,0,0
  end if
End Sub

In the open event of the Window, we’ll set everything up:

Sub Open()
  ' setup the buffer
  pBuffer = New Picture(Canvas1.Width, Canvas1.Height, 32)
  gBuffer = pBuffer.Graphics

  ' setup the connections bitmap
  CurrentConnections = New Picture(Canvas1.Width, Canvas1.Height, 32)

  ' try = DRAWBEZIER to see another effect
  DrawMethod = DRAWCATENARY

  ' Important! Set lastColor to 2 so even numbers, not = 0, are startpoints, odd numbers are endpoints
  ConnectionLastColor = 2

  ' draw it
  DrawCables
End Sub

We initialized our Buffer and also our CurrentConnections bitmap (the third picture from above). It has the same dimensions as the canvas (and background). We pick a Drawing method (here the Catenary type) and a help variable for identifying the start and end points of each cable we will draw. As the background is black, we’ll start at 2 so our starts will be even numbers (not being 0) and our odd numbers will be the end points. Finally, we draw the cables (in this case, none are defined, so just the background).

I’ll come back to the DrawCables function in a moment. First let’s check the canvas mouse events:

Function MouseDown(X As Integer, Y As Integer) As Boolean
  ' check if it is an existing cable
  dim c as ABCable
  c = CheckCables(X,Y)
  if c <> nil then
    BringToFront(c)
    DrawCables
  end if

  Return true
End Function

Sub MouseDrag(X As Integer, Y As Integer)
  if IsDraggingEnd then
    Cables(UBound(Cables)).SetEndPoint(new ABPoint(X,Y))
    DrawCables
    me.MouseCursor = System.Cursors.InvisibleCursor
  Elseif IsDraggingStart then
    Cables(UBound(Cables)).SetStartPoint(new ABPoint(X,Y))
    DrawCables
    me.MouseCursor = System.Cursors.InvisibleCursor
  end if
End Sub

Sub MouseUp(X As Integer, Y As Integer)
  if IsDraggingStart or IsDraggingEnd then
    dim col as Color
    col = CurrentConnections.Graphics.Pixel(X,Y)
    if col <> &c000000 then ' it's used by another one, cannot be connected
      Cables.Remove(UBound(Cables))
    else
      ' is it a connection point?
      col = BackgroundConnections.Graphics.Pixel(X,Y)
      if col <> &cFFFF00 then ' not yellow, so can not be used to connect a cable
        Cables.Remove(UBound(Cables))
      end if
    end if
    IsDraggingStart = false
    IsDraggingEnd = false
    DrawCables
  end if

  me.MouseCursor = System.Cursors.StandardPointer
End Sub

In the MouseDown we’ll check if we have an existing cable or if we have to create a new one. This is done with the CheckCables() function. This function uses the CurrentConnections picture to check which, if any, cable you clicked on. We grab the pixel at postion X,Y and if it’s not black, we’ve touched a start or end point of a cable. If the color is even, we have a start point so we set IsDraggingStart = true. If it is odd, we’ll set IsDraggingEnd = true.

If it is black, we’ll check if it may be a possible connection point. We grab the pixel at position X,Y in the BackgroundConnections picture. If it is yellow, we can create a new Cable. We always assume you’re dragging the end of the cable to we’ll set IsDraggingEnd = true.

In both cases, we return the existing cable, the new cable or nil if you just clicked on the background. Check out the code to see how it works. I’ve added comments to make it easier to follow.

Function CheckCables(X as integer, Y as integer) As ABCable
  Dim c as ABCable

  dim col as Color
  dim colInt as Integer
  ' check if a cable is already connected to this connection point
  col = CurrentConnections.Graphics.Pixel(X,Y)
  if col <> &c000000 then ' it's an existing one
    for each c in Cables
      if c.StartPointColor = col or c.EndPointColor = col then
        ' found the cable
        colInt = ColorToInteger(col)
        if colInt mod 2 = 0 then
          ' is the start
          IsDraggingStart = true
        else
          ' is the end
          IsDraggingEnd = true
        end if
        Return c
      end if
    next
  else ' it's a new one, can it be connected
    col = BackgroundConnections.Graphics.Pixel(X,Y)
    if col = &cFFFF00 then ' yellow, so can be used to connect a cable
      dim i as Integer
      ' pick a random color for the cable
      dim NewCableColor as Color
      i = rnd*4
      select case i
      case 0
        NewCableColor = RGB(242, 131, 42)
      case 1
        NewCableColor = RGB(97,149,102)
      case 2
        NewCableColor = RGB(110,196,196)
      case 3
        NewCableColor = RGB(206,73,78)
      end select
      ' create a new cable
      c = new ABCable(new ABPoint(X,Y), new ABPoint(X,Y), NewCableColor,400,30)
      ' set its start end end color for the CurrentConnections bitmap to trace the mouse
      c.StartPointColor = IntegerToColor(ConnectionLastColor)
      c.EndPointColor = IntegerToColor(ConnectionLastColor+1)
      ' update the new last color for the next cable
      ConnectionLastColor = ConnectionLastColor + 2
      ' add the cable
      Cables.Append c
      ' always assume it's the end
      IsDraggingEnd = true
    end if
  end if

  Return c
End Function

Now we return to the MouseDown() event. If we have a Cable (c not nil), bring this cable to the front and draw again.

When we pass through the MouseDrag() event, one of our ends (start or end) will be active. Depending on which one it is, we’ll adjust the position by using SetStartPoint and SetEndPoint of the last Cable in our stack. Because we used the BringToFront() function, the last one in the stack is always our current one.

In the MouseUp() event we’ll do something similar as in the CheckCables() function. Now we first check if nobody is yet connected to this connection point (CurrentConnections.Graphics.Pixel(X,Y) not &c000000). In this case, we’ll just remove the cable. If it is black, we’ll check if it is a possible connection point (BackgroundConnections.Graphics.Pixel(X,Y) not &cFFFF00 ‘ yellow). If it is something else than yellow, remove the cable also.

Reset the Dragging variables to false and Draw the cables.

In the DrawCables() function, we’ll do five things:
1. Setup our GDIPlusGraphics for windows or our CGContextGraphicsPort for OSX if not done yet
2. We draw the background
3. We reset the CurrentConnection picture
4. We’ll draw all the cables on it
5. We’ll draw the current Start and End points of the cables to our CurrentConnection picture

Sub DrawCables()
'1. SETUP + 2. DRAW THE BACKGROUND
  #if TargetWin32
    if WINGfx = nil then
      dim s as Status
      WINGfx =  new GdiPlusGraphics( gBuffer.Handle( Graphics.HandleTypeHDC ) )
      s = WinGfx.SetInterpolationMode(InterpolationMode.High)
      s = WinGfx.SetCompositingQuality(CompositingQuality.High)
      s = WinGfx.SetSmoothingMode(SmoothingMode.AntiAlias)
    end if
    gBuffer.DrawPicture BackGround,0,0
  #else
    if OSXGfx = nil then
      OSXGfx = new CGContextGraphicsPort( gBuffer )

      OSXGfx.SetSAllowsAntialiasing(true)
      OSXGfx.SetShouldAntialias(true)

      OSXGfx.TranslateCTM(0, gBuffer.Height)
      OSXGfx.ScaleCTM(1.0, -1.0)
    end if
    OSXGfx.SaveGState
    OSXGfx.TranslateCTM(0,gBuffer.Height)
    OSXGfx.ScaleCTM(1.0, -1.0)
    dim r as CGRect = CGRectMake(0,0,gBuffer.Width, gBuffer.Height)
    if ImageOSX = nil then
      dim f as FolderItem
      dim p as Picture
      f = GetFolderItem("Background.png")
      if f.Exists then
        ImageOSX = CGImage.NewCGImage(p.Open(f))
      end if
    end if
    OSXGfx.DrawImage(ImageOSX, r)
    OSXGfx.RestoreGState
  #endif

' 3. CLEAR THE CURRENT CONNECTIONS
  CurrentConnections.Graphics.ForeColor = &c000000
  CurrentConnections.Graphics.FillRect 0,0, CurrentConnections.Width, CurrentConnections.Height

' 4. DRAW ALL OUR CABLES + 5. DRAW OUR START AND ENDPOINTS
  dim c as ABCable
  for each c in Cables
    c.Draw(gBuffer, DrawMethod)
    ' if it's not the current cable
    if c <> Cables(UBound(Cables)) then
      CurrentConnections.Graphics.ForeColor = c.StartPointColor
      CurrentConnections.Graphics.FillOval(c.StartPoint.x-6, c.StartPoint.y-6, 12,12)
      CurrentConnections.Graphics.ForeColor = c.EndPointColor
      CurrentConnections.Graphics.FillOval(c.EndPoint.x-6, c.EndPoint.y-6, 12,12)
    else
      'if it is, see if it is being dragged
      if not IsDraggingStart then
        CurrentConnections.Graphics.ForeColor = c.StartPointColor
        CurrentConnections.Graphics.FillOval(c.StartPoint.x-6, c.StartPoint.y-6, 12,12)
      end if
      if not IsDraggingEnd then
        CurrentConnections.Graphics.ForeColor = c.EndPointColor
        CurrentConnections.Graphics.FillOval(c.EndPoint.x-6, c.EndPoint.y-6, 12,12)
      end if
    end if
  next
  Canvas1.Refresh(false)
End Sub

The result is this:

ConnectionPoints Principle

Click to enlarge

The first is the result in our Buffer. The second one is the result of the CurrentConnections Drawings. We’ll see the start and end points. The third picture is just to show you where each point in CurrentConnections comes from. So now you see when we use the CheckCables function, we can know exactly which cable and at what end we clicked by checking the pixel color of currentConnections.

Note: the colors in 2 and 3 are faked to make it clear how the principle works. In the real thing, all points are only one integer away from each other, but this is not very visible for us.

And that’s it! There are some help functions like IntegerToColor, ColorToInteger and an extra MouseMove event to hide the cursor but they need no explanation.

This was a fun project I think. Let’s look how it goes in this video:

You can download the full source code here:
http://gorgeousapps.com/ABCablesOSXWINXojo.zip

This concludes the cable and chains tutorial.

Alwaysbusy

Click here to Donation if you like my work

Advertisements

About Alwaysbusy

My name is Alain Bailleul and I'm the Senior Software Architect/Engineer at One-Two. I like to experiment with new technologies, Computer Vision and A.I. My projects are programmed in B4X , Xojo, C#, java, HTML, CSS and JavaScript. View all posts by Alwaysbusy

One response to “Xojo: About cables, chains and other curves…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: