Tuesday, 17 March 2015

FreeCAD: Shell Ecomarathon Prototype Cover

 ... or FreeCAD to the real world without 3d printing, but a 3 meters CNC mill:

 

Updated ( because we won!! :D )


Prototype Dátil 15 wins the European SEM on Ethanol



 

UPDATED: 


The result: 





On the news

 The story:

I am member of the UMH Team, a group of students that build a prototype car to race at the Shell Ecomarathon, an event that this year takes place at Rotterdam. Currently, we are in the first position of the spanish championship, leading it in the combustion category, with a fuel consumption of 1223 kilometers / litre of ethanol. In the European competition, we have been at the top ten, but past edition we finished 13th at combustion category and 3rd on ethanol.

This year we are building a completely new car that we hope bring us to the podium.


What I am showing here is the cover of the new prototype, that I designed using FreeCAD, simulated using SolidWorks, and later converted to CNC code using Hypermill. Special thanks go to Volund Group for their help and support.

The FreeCAD part:

The cover is designed using already existing tools, basically is a loft going through several sketches that I placed using a simple script.

This is the first one I did, just to test if FreeCAD was able to acomplish this work.



The car frame was made with Autodesk Inventor and imported as IGES, then I started to create the sketch-ribs and finally a loft through all them. I adjusted the sketches to obtain the desired shape. Once happy with it, I made the windows and a basic visibility test to ensure we are compliance with the rules of the race.

The rules specify that a pilot must be able to see a polar array of cylinders that are around the car with an angle of 30 degrees between each one.



Thanks to FreeCADs perspective view, I could check window sizes and do some optimization.

This is the pilot's point of view


Simulation:

The covering was simulated using SolidWorks, resulting in a Cx of 0.33.



Milling:

Once we performed another tests and confirmed that everything was ready, we started to machine a foam block to create a positive mould. We have used a medium-sized CNC milling center (Volund Group) that can handle the 3000 mm of length of this car:


Raw block:



Rough pass:



Smoothing pass:

 

The smoothing pass is finishing as I write this. What we are doing next is to epoxy and fiberglass it to create the negative mould and then, do the carboon fiber cover. I will edit this post to upload finished pictures.


I hope this serves an example of real world usage of FreeCAD, beyond 3D printing.

Maybe I do a tutorial to show how to model surfaces like this prototype cover, stay tuned. 

Bye!


Sunday, 21 December 2014

FreeCAD: Mechanical 3D scanner using Arduino

Scanning 3d objects mostly relies on several cameras and complex software, so it is not a low budget project. But I want to show you this quick idea I got to virtualize real models using scrap parts, Arduino and FreeCAD:


A 3 degrees of freedom arm that knows its position by the variable resistors that form its joints.
The arduino reads this resistors and prints the code by serial, where a python script running inside FreeCAD waits for the data.



I'm impressed with its accuracy because it recreates the real world objects with some kind of precision, in spite of being poorly built.

This photo is an example:



The propeller seen in the first picture:



With a better built arm, I'm sure this can improve enough to be usable.

Sunday, 14 December 2014

FreeCAD: Sheet metal idea part 2

UPDATE 2:  Document structure and basic tools
-
UPDATE: Ulrich has developed an unfolding algorithm that can be used as a macro.
-

Some things have changed since sheet metal part 1, where, for example, faces were classified by being flat or cylindrical, or folds only counted if their angle was 90º.

Even thought the algorithm is not finished (current problem is being discussed here), the new approach I'm going to explain here is more robust and is not even limited to face geometry or fold angles. You can even do folds with different radius, this face classifier can withstand it.

This is the part I'm going to use as example:


It has been created using this script, and you can download it here.

The workflow that I have in mind to unfold a part is:

- Select the face that will behave as base (all faces will be unfolded over the plane by it described )
- Run the script.

Let's start.

Import the libraries:

As always, we need to import some library. The only one we need is "Gui", from the  main FreeCAD library. In addition, we set the thickness of the sheet:

from FreeCAD import Gui
thk = 1.0

Retrieve user selection:

This is a basic command very useful at writing macros:

# Get Selected Shape and Face
SelObject = Gui.Selection.getSelection()[0].Shape
SelFace = Gui.Selection.getSelectionEx()[0].SubObjects[0]

SelObjects contains the selected shape and SelFace contains the selected face. Note that user only needs to click on the face and with that we can take the whole object.

Get ride off non useful faces:

The action starts here, but first: Which are the non-useful faces?
They are the ones marked in green at the picture:


This faces are not needed because they do not store useful geometry data. To clear things in a future, we are going to create a list excluding this faces:

faceList = [] # Create an empty list that will hold the filtered faces
for face in SelObject.Faces:
  apnd = True
  for edge in face.Edges: # Measure length of all edges from all faces
    if abs( edge.Length - thk ) < 0.001: #tolerance because float point things
      apnd = False # If edge length is equal to thickness, do not append it.
      break
  
  if apnd:  # If the face hasn't any edge with length == thickness, append it.
    faceList.append( face )


Did it work? Is something easy to test: select a face from the object and paste at FreeCAD's python console all the code.
Then, do:

len( SelObject.Faces )

len( faceList )


The first one gives "46" and the second "18". That's a great data reduction!

Unfold tree: step 1

We have a list containing only meaningful data, but this data is duplicated. Why? 






Is duplicated because the faces selected in the first picture (back of the part) are parallel to the faces on the second picture (top of the part). While is easy to create another filter to remove the repeated faces, is very hard to not lose the connection between faces with it.

So we are going to create a list containing wich face links to wich face or group of faces. This list has this form:

    index       A        B        C        D        E        F
     "list" =  [ [B],[A,C,D],[B],  [B,E,F], [D],    [D] ]

The index number belongs to the position in "faceList" of the face. The content under the index are the faces linked to that face.
To explain it better, "list" is the solution to faces links of this simplified part:



So how we create a list like that one?
Just run this little "monster":

# auxiliar function: find face number (index) in faceList:
def gfN( inFace ):
  SF_COM = inFace.CenterOfMass
  SF_NOR = inFace.normalAt( 0, 0 )
  for n in range( len( faceList ) ):
    F_COM = faceList[n].CenterOfMass
    F_NOR = faceList[n].normalAt(0,0)
    if SF_COM == F_COM and SF_NOR == F_NOR:
      break

  return n

# get selected face position (index)  in faceList:
SelFaceNumber = gfN( SelFace )

# auxiliar function: get faces linked to input face (returns position in faceList )
def gfR( inFaceN ):
  temporalList0 = []
  inFace = faceList[inFaceN]
  for inEdge in inFace.Edges:
    if str( inEdge.Curve )[1:5] == "Line":
      P_ia = inEdge.valueAt( 0.0 )
      P_ib = inEdge.valueAt( inEdge.Length )
      V_0 = ( P_ib - P_ia )
      for n in range( len( faceList ) ):
        if n != inFaceN:
          face = faceList[n]
          for edge in face.Edges:
            P_a = edge.valueAt( 0.0 )
            P_b = edge.valueAt( edge.Length )
            V1 = P_b - P_ib
            V2 = P_a - P_ia
            condition0 = abs( ( V_0.cross( V1 ) ).Length ) < 0.0001
            condition1 = abs( ( V_0.cross( V2 ) ).Length ) < 0.0001
            if condition0 and condition1:
              faceNumber = gfN( face )
              temporalList0.append( faceNumber )
              break

  # clean from repeated faces
  temporalList1 = []
  for i in temporalList0:
    if not( i in temporalList1 ):
      temporalList1.append( i )

  return temporalList1



compFaceRel = []
for fn in range( len( faceList ) ):
  data = gfR( fn )
  compFaceRel.append( data )


If you run the code above, and then type "comFaceRel" you should see something like:

[[3, 2], [4, 5], [0, 3, 6], [0, 7], [1, 8], [1, 4, 9], [2], [3, 10, 11], [4], [5, 12, 13], [7, 14], [7, 10, 15], [9, 13, 16], [9, 17], [10], [11], [12], [13]]

How a link is recognized:



faceA is the current face being analyzed to inspect its links. FaceB is one of all the faces contained in facesList suspicious of being linked with faceA. 
V_0 the director vector of the current edge of faceA being inspected. V1 and V2 are vectors going from the end-points of the edge from faceA to the end-points of an edge owned by faceB. 
A face is linked if the cross products V1xV0 and V2xV0 have a length of 0.

All this is calculated inside the previous function "gfR".


At the moment, this is the current state of the sheet metal workbench. Next step is to get the link between final nodes of the tree of faces to the selected face to unfold. 
Taking the previous example model with faces A, B, C..., the needed list previous to unfold is, with B as unfold base:
                                                  [ [A,B], [C,D], [F,D,B], [E,D,B]]

Knowing that list would be the major step forward. With it, the unfold algorithm would be almost finished.


Complete code:

# JMG december 2014
from FreeCAD import Gui

thk = 1.0

# Get Selected Shape and Face
SelObject = Gui.Selection.getSelection()[0].Shape
SelFace = Gui.Selection.getSelectionEx()[0].SubObjects[0]

# remove faces placed on the thickness
faceList = [] # Create an empty list that will hold the filtered faces
for face in SelObject.Faces:
  apnd = True
  for edge in face.Edges: # Measure length of all edges from all faces
    if abs( edge.Length - thk ) < 0.001: #tolerance because float point things
      apnd = False # If edge length is equal to thickness, do not append it.
      break
  
  if apnd:  # If the face hasn't any edge with length == thickness, append it.
    faceList.append( face )


# auxiliar function: find face number (index) in faceList:
def gfN( inFace ):
  SF_COM = inFace.CenterOfMass
  SF_NOR = inFace.normalAt( 0, 0 )
  for n in range( len( faceList ) ):
    F_COM = faceList[n].CenterOfMass
    F_NOR = faceList[n].normalAt(0,0)
    if SF_COM == F_COM and SF_NOR == F_NOR:
      break

  return n

# get selected face position (index)  in faceList:
SelFaceNumber = gfN( SelFace )

# auxiliar function: get faces linked to input face (returns position in faceList )
def gfR( inFaceN ):
  temporalList0 = []
  inFace = faceList[inFaceN]
  for inEdge in inFace.Edges:
    if str( inEdge.Curve )[1:5] == "Line":
      P_ia = inEdge.valueAt( 0.0 )
      P_ib = inEdge.valueAt( inEdge.Length )
      V_0 = ( P_ib - P_ia )
      for n in range( len( faceList ) ):
        if n != inFaceN:
          face = faceList[n]
          for edge in face.Edges:
            P_a = edge.valueAt( 0.0 )
            P_b = edge.valueAt( edge.Length )
            V1 = P_b - P_ib
            V2 = P_a - P_ia
            condition0 = abs( ( V_0.cross( V1 ) ).Length ) < 0.0001
            condition1 = abs( ( V_0.cross( V2 ) ).Length ) < 0.0001
            if condition0 and condition1:
              faceNumber = gfN( face )
              temporalList0.append( faceNumber )
              break

  # clean from repeated faces
  temporalList1 = []
  for i in temporalList0:
    if not( i in temporalList1 ):
      temporalList1.append( i )

  return temporalList1



compFaceRel = []
for fn in range( len( faceList ) ):
  data = gfR( fn )
  compFaceRel.append( data )




Bye!