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!

Thursday, 11 December 2014

FreeCAD: Sheet metal tool "Add Wall"

 Thought not seen here, I work in the sheet metal workbench from time to time (last update), being my greater effort coding the unfold algorithm (related post). But I also work at the surrounding tools that this workbench needs, one of them is almost usable and is the reason of this post.

Add wall tool:


When I create the "dummy" shapes for testing the unfolding algorithm, the process is somewhat tedious. So I created this tool and developed it a bit to be easily integrated in the workbench in a future.

The utility of the tool is explained with this video:



Code can be found here

How to use it:

The script contains the function  "addWall", when calling it this are the possible input parameters:

thk = float:           Sheet thickness, 1.0 is the default value
bendR =  float:     Bend radius, default value is 3.0
alpha = float:       Angle of the new wall plane in reference to previous shape plane. 0 will give error (infinite bending radius needed ). By default 90º

L1 and L2 are better explained with a picture:


L1 and L2 values (float) modify the lengths noted by the white dimension marks. By default 0.0

Relief = boolean:  Create or not square relief slots (I think this is the correct name, but tell me if not) like this:


Relief slots have two parameters:

rlfWidth = float: The width of the relief, by default 0.5
rlfDepth = float: The depth of the relif, by default 1.0

Other parameters are:

inverted = boolean: Reverse bend direction, false by default
create = boolean:  This is to merge new wall with current object, true by default. Workbench utility
sketch = boolean: Automatically places a new sketch on new wall, false by default.


To use it, copy-paste the code at FreeCAD's  python console, select one suitable face to place a new wall and then type "addWall( arguments )".

It should work.


Bye!

Friday, 14 November 2014

FreeCAD: Tour camera

Imagine that you draw a house or a building in FreeCAD with all its details and interior elements. Wouldn't it be awesome if you could get into just like if you were walking inside?
This "tour camera" macro makes it possible.



This is the small "town" I've created for testing this macro. Download it here, get the full macro
code at the bottom of the post, and follow this microtutorial



(notice the camera track in orange)

How does it work?

Very simple, create a sketch over the surface you want to travel and exit. Then, switch view mode to "perspective view", and finally,  at the previous sketch, select one line and paste this at the python console:

from pivy import coin
import time
from FreeCAD import Base
cam = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
trajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges
for edge in trajectory:
  startPoint = edge.valueAt( 0.0 )
  endPoint = edge.valueAt( edge.Length )
  dirVector = ( endPoint - startPoint ).normalize()
  currentPoint = startPoint
  while (currentPoint - startPoint).Length < edge.Length:
    currentPoint = currentPoint + dirVector
    cam.position.setValue(currentPoint + Base.Vector( 0,0, 10) )
    cam.pointAt( coin.SbVec3f( endPoint[0], endPoint[1], endPoint[2]+10) , coin.SbVec3f( 0, 0, 1 ) )
    Gui.updateGui()
    time.sleep(0.005)

What it does is:

-Gets camera node and sketch track as a list of edges
-Iterates over the list of edges, positioning the camera from edge start to the edge length position, looking at the edge endpoint.

If you did the above, you should be travelling like this:



But is not only your screen the one that shakes badly when you go from one line to the next, mine too.
When camera jumps from one line to the next, it is orientated to the new endpoint in one step. That's the origin of the shaking behavior.

So, with a bit more of complication, I've improved this (new "town" included :):


In this version, camera walks along the actual edge pointing its view to a vector, with a given length and the same direction than the movement. Once this vector reaches the intersection with the next line, the camera rotates to align with the end point of that line. 
In conclusion, you get a smooth transition and a nice walk around.

But that's the behavior roughly speaking, for the real thing, please look the code below.

# JMG November 2014
from pivy import coin
import time
from FreeCAD import Base
camera = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
trajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges
camHeight = 10   # Height of the camera above the track
lookVectorLength = 80   # Distance from next line start where the camera starts to align with new direction
for i in range( len( trajectory ) - 1):
  currEdge = trajectory[i]
  currEdgeDir = ( currEdge.valueAt( currEdge.Length ) - currEdge.valueAt( 0.0 ) ).normalize()
  nextEdge = trajectory[i+1]
  nextEdgeDir = ( nextEdge.valueAt( nextEdge.Length ) - nextEdge.valueAt( 0.0 ) ).normalize()
  currPos = currEdge.valueAt( 0.0 )
  while (currPos - currEdge.valueAt( 0.0 ) ).Length < currEdge.Length:
    currPos = currPos + currEdgeDir
    camera.position.setValue( currPos  + Base.Vector( 0, 0, camHeight ) )
    cameraLookVector = currEdgeDir*lookVectorLength
    if (cameraLookVector + currPos - currEdge.valueAt(0.0) ).Length > currEdge.Length:
      L = ( cameraLookVector + ( currPos - currEdge.valueAt( 0.0 ) ) ).Length - currEdge.Length
      lookVector = nextEdgeDir*L + nextEdge.valueAt( 0.0 )
      
    else:
      lookVector = currEdge.valueAt( currEdge.Length )
    
    camera.pointAt( coin.SbVec3f( lookVector[0], lookVector[1], lookVector[2] + camHeight ), coin.SbVec3f( 0, 0, 1 ) )
    Gui.updateGui()
    time.sleep( 0.004 )



It features two configurable parameters:

- camHeight: height of the camera above the track ( over z axis )

-lookVectorLength: distance from the endpoint of the current line at witch the camera starts to rotate to align with the next line. Try values, the more it approaches to 0, the more shake you get. But too big values will shake too!

Before leaving, a funny thing: instead of the sketch track, select the 3d model and paste the code.

Code update:

A complex track may involve booleans, and , as every time you operate with a shape their subelements get reorganized, the result is something like what .rpv got trying to tour inside a 3d printer. Curious as a background video in presentations, but not the expected behavior.

The solution is to rearrange the list of edges so next edge start begins at the current edge end. It is achieved this way:

SelectedEdge = Gui.Selection.getSelectionEx()[0].SubObjects[0]
RawTrajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges

# Edge rearrangement inside trajectory list
trajectory = []
trajectory.append( SelectedEdge )
currentEdge = SelectedEdge
for n in range( len( RawTrajectory ) ):
  for edge in RawTrajectory:
    if edge.valueAt(0.0) == currentEdge.valueAt( currentEdge.Length ):
      trajectory.append( edge )
      currentEdge = edge
      break


Full code:

Complete version featuring everything:

# JMG November 2014
from pivy import coin
import time
from FreeCAD import Base
camera = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
SelectedEdge = Gui.Selection.getSelectionEx()[0].SubObjects[0]
RawTrajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges

# Edge rearrangement inside trajectory list
trajectory = []
trajectory.append( SelectedEdge )
currentEdge = SelectedEdge
for n in range( len( RawTrajectory ) ):
  for edge in RawTrajectory:
    if edge.valueAt(0.0) == currentEdge.valueAt( currentEdge.Length ):
      trajectory.append( edge )
      currentEdge = edge
      break

camHeight = 10   # Height of the camera above the track
lookVectorLength = 80   # Distance from next line start where the camera starts to align with new direction
for i in range( len( trajectory ) - 1):
  currEdge = trajectory[i]
  currEdgeDir = ( currEdge.valueAt( currEdge.Length ) - currEdge.valueAt( 0.0 ) ).normalize()
  nextEdge = trajectory[i+1]
  nextEdgeDir = ( nextEdge.valueAt( nextEdge.Length ) - nextEdge.valueAt( 0.0 ) ).normalize()
  currPos = currEdge.valueAt( 0.0 )
  while (currPos - currEdge.valueAt( 0.0 ) ).Length < currEdge.Length:
    currPos = currPos + currEdgeDir
    camera.position.setValue( currPos  + Base.Vector( 0, 0, camHeight ) )
    cameraLookVector = currEdgeDir*lookVectorLength
    if (cameraLookVector + currPos - currEdge.valueAt(0.0) ).Length > currEdge.Length:
      L = ( cameraLookVector + ( currPos - currEdge.valueAt( 0.0 ) ) ).Length - currEdge.Length
      lookVector = nextEdgeDir*L + nextEdge.valueAt( 0.0 )
      
    else:
      lookVector = currEdge.valueAt( currEdge.Length )
    
    camera.pointAt( coin.SbVec3f( lookVector[0], lookVector[1], lookVector[2] + camHeight ), coin.SbVec3f( 0, 0, 1 ) )
    Gui.updateGui()
    time.sleep( 0.004 )



Enjoy!

Saturday, 13 September 2014

FreeCAD: simple Coin3d plot

Hi all: I'm back!

Some time ago, when I was investigating about octrees and voxels to create a true CNC simulator, I played a bit with Coin3d, the scene graph render of FreeCAD.
Coin3d is coded in C++, but can be accesed with Python using Pivy.

While I didn't find anything specially useful, I realized that one could easily plot math equations like this:


...which have a nice look and permit you to take curious snapshots, for example:



The code is:

from pivy import coin
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
print sg
for s in range(5):
  for n in range(-25,25):
    for i in range(-25,25):
      col = coin.SoBaseColor()
      col.rgb=(i*2,n*2,s*2)
      trans = coin.SoTranslation()
      # equation: z = f( i, n ) s is to plot at several heights
      trans.translation.setValue([i*2.5,n*2.5,(i/2.0)**2+(n/2.0)**2+s*5])
      cub = coin.SoSphere()
      myCustomNode = coin.SoSeparator()
      myCustomNode.addChild(col)
      myCustomNode.addChild(trans)
      myCustomNode.addChild(cub)
      sg.addChild(myCustomNode)



More info about FreeCAD and Coin3D, here

Bye!

Thursday, 28 August 2014

What's going on this summer

I'm having september exams, and that's the reason for not publishing anything remarkable this month. But that doesn't mean I've stopped doing FreeCAD stuff.

This is what is currently going on:


A GCode generator for 2D CNC cutter. Is very experimental, but starts to show results. (green fast movement, red = given feed speed)

I've also worked with the SheetMetal project, where I re-coded a big part of it. I hope to finish it somewhere around next year.

Related with 2D CNC, I remodelled my crappy machine and did this:


It does more less what is intended. The weird result can be half attributed to the machine and the other half to the code. I coded a bit more after that video, now it draws better. 

The machine is working with an arduino DUE and three 4899et. It talks with FreeCAD by serial (USB), with a custom code running in the arduino.


I also tried to create a game using FreeCAD, basically a model of a car controlled by joystick. Is almost working, but because of its non-sense nature, is the lower on my preferences list.


Car model


Bye!

Thursday, 24 July 2014

FreeCAD: Sheet metal idea part 1

A sheet metal workbench will be a great addition to FreeCAD. In fact, there are several posts in the forum wishing for it.
Here I show my modest attempt to create such functionality for FreeCAD, in a very early stage of development.

I've divided the algorithm in three stages:

Part 1: Explore the shape
Part 2: Get the particular geometry of every face
Part 3: Unfold

Once it works correctly, I will try to code a modelling tool to add walls, perforations, and standard elements. (UPDATED: See sheet metal idea part 2 for improved algorithms and further info )

Part 1

As said above, here I`ll try to filter an input shape that meets some conditions, let's go.

First: create the 3d object

The sheet object is created manually at the moment. In a future, a specific tool can be developed to make it easier. If you want to jump this step, get the final model here

In a new document, create a sketch on XZ:


What we are going to build there is the profile of the folded sheet, so thickness and the bending radius are set here. 
We can start by drawing the folds:


They must be concentric and the inner one should measure the bending radius.

Now we draw the rest of it:


Applying the correspondent constrains you should obtain something like this:


Where we set the thickness of the sheet to 2 mm and the bending radius to 8 mm.

Now, extrude the sketch:



For example, 100 mm.


Now create a hole on top and cut the sheet to change its square form:


Pocket the sketch and this is the result:



The algorithm:

The algorithm works by exploring the shape with some variables in mind, like bending radius, thickness, 90º degree bending angles and that the part is based on XY plane.
Use it by selecting the shape on the tree-view and copy-paste the code.

Import the libraries:

import Part
import math as mt

Basic definitions needed:

Thickness = 2.0
BendingRadius = 8
k = 0.33
Alpha = 90.0

Get user selection:

SObj = Gui.Selection.getSelection()[0]
SObj_Shape = SObj.Shape

Create empty lists:

Faces = []
FlatFaces = []
CylFaces = []

Get all faces of the selected object and gather them in the list "Faces"

for i in SObj_Shape.Faces:
  Faces.append(i)


Classify the gathered faces by being flat or cylindrical:

for i in Faces:
  Surface = i.Surface
  if str(Surface) == "<Plane object>":
    FlatFaces.append(i)
  if str(Surface) == "<Cylinder object>":
    CylFaces.append(i)


At the moment we have all the faces of the shape classified by being cylindrical (bends) and flat. 

The next step is to remove the faces marked on the picture, because we do not need them


To do it:

RemoveFaces = []

for i in FlatFaces:
  for n in i.Edges:
    Len = n.Length
    if Len > Thickness*0.99 and Len < Thickness*1.01:
      RemoveFaces.append(i)
      break

It searches for faces which have one of their edges equal to the sheet thickness (with a tolerance, to ride off floats inaccuracy) and appends them to the new list RemoveFaces.

for i in RemoveFaces:
  FlatFaces.remove(i)

With that sentence the non desired faces are removed from the main list "FlatFaces"

The next faces to remove are the parallel ones, we need only one of them:




This works this way:

-Get the center of mass of a face
-Get the center of mass of another face
-Are them separated by the sheet thickness?
-If they are, append one of them to RemoveFaces

RemoveFaces = []
for i in FlatFaces:
  C1 = i.CenterOfMass
  for n in FlatFaces:
    C2 = n.CenterOfMass
    V12 = C2 - C1
    M12 = abs(V12.Length)
    if M12 > Thickness*0.99 and M12 < Thickness*1.01:
      FlatFaces.remove(n)
      break

for i in RemoveFaces:
  FlatFaces.remove(i)



To finish this post (I've more coded, future posts about this will come ;) ), a test to see what is in the list "FlatFaces":

def TESTF(FlatFaces):
  for i in FlatFaces:
    center = i.CenterOfMass
    Origin = center
    Origin_Vertex = Part.Vertex(Origin)
    Origin = App.ActiveDocument.addObject("Part::Feature","Test_Point")
    Origin.Shape = Origin_Vertex
    Origin_User_Name = Origin.Label
    FreeCADGui.ActiveDocument.getObject(Origin_User_Name).PointColor = (0.33, 0.00, 1.00)
    FreeCADGui.ActiveDocument.getObject(Origin_User_Name).PointSize = 5.00


The function input is a list containing faces. It draws a point at the center of mass of every face of the list, and if we apply it to our "FlatFaces" list we obtain:


That means we had a success at filtering the input shape!

Next steps are gather what is inside face (hole, squares...) and unfold.

Part 1 complete code:

"""
Javier Martinez Garcia, 2014
"""
import Part
import math as mt

Thickness = 2.0
BendingRadius = 8
k = 0.33
Alpha = 90.0

SObj = Gui.Selection.getSelection()[0]
SObj_Shape = SObj.Shape

Faces = []
FlatFaces = []
CylFaces = []

for i in SObj_Shape.Faces:
  Faces.append(i)

for i in Faces:
  Surface = i.Surface
  if str(Surface) == "<Plane object>":
    FlatFaces.append(i)
  if str(Surface) == "<Cylinder object>":
    CylFaces.append(i)

RemoveFaces = []

for i in FlatFaces:
  for n in i.Edges:
    Len = n.Length
    if Len > Thickness*0.99 and Len < Thickness*1.01:
      RemoveFaces.append(i)
      break

for i in RemoveFaces:
  FlatFaces.remove(i)

RemoveFaces = []
for i in FlatFaces:
  C1 = i.CenterOfMass
  for n in FlatFaces:
    C2 = n.CenterOfMass
    V12 = C2 - C1
    M12 = abs(V12.Length)
    if M12 > Thickness*0.99 and M12 < Thickness*1.01:
      FlatFaces.remove(n)
      break

for i in RemoveFaces:
  FlatFaces.remove(i)

def TESTF(FlatFaces):
  for i in FlatFaces:
    center = i.CenterOfMass
    Origin = center
    Origin_Vertex = Part.Vertex(Origin)
    Origin = App.ActiveDocument.addObject("Part::Feature","Test_Point")
    Origin.Shape = Origin_Vertex
    Origin_User_Name = Origin.Label
    FreeCADGui.ActiveDocument.getObject(Origin_User_Name).PointColor = (0.33, 0.00, 1.00)
    FreeCADGui.ActiveDocument.getObject(Origin_User_Name).PointSize = 5.00


TESTF(FlatFaces)

Feel free to criticize or point out anything you consider ;)

Bye!

Friday, 11 July 2014

FreeCAD + Arduino

Modifying shapes by reading sensors can create a lot of possibilities, for example, this video:



An Arduino DUE with a varible resistor commands a servo inside FreeCAD.


This post is about making what is shown in the video.

What you need:


-Linux

-Any Arduino board with serial connection

-Arduino ide

-FreeCAD

- My servo model

-Pyserial library ( sudo apt-get install python-serial )

-A look at the Oficial arduino and python guide ( not really needed, but my main source )

The idea:

-Print the value of the sensor by serial
-Create a function that reads the Arduino serial
-Create another function that, with the value of the serial, updates the object in FreeCAD
-Call them repetitively by a timer

The Arduino part:

At the video I'm using an Arduino DUE because it was handy, but an UNO board is valid too.

The electrical thing consists of connecting a variable resistor to the A0 pin, like the scheme:


Any variable resistor above 1kΩ will do the job, in the video I use a 4.7kΩ one. 

The code is quite simple:
void setup()
{
  Serial.begin(9600);                  /// Start serial at 9600 baud
  pinMode( A0, INPUT );                /// Set pin A0 as input
}

void loop()
{
  Serial.println( analogRead( A0 ) );  /// Print to serial A0 value
}

Initialize serial and then constantly print the sensor value.

The Python code:

This are the needed libraries:

import serial
from PySide import QtCore

Serial for reading the serial (obvious one) and PySide for the timer object.

To initialize the serial at 9600 baud pointed to the Arduino:

ser = serial.Serial('/dev/ttyACM0', 9600)

The Arduino serial should be in '/dev/ttyACM0' but sometimes it switches to '/dev/ttyACM1',
if you fire up the Arduino Ide and open the serial window, the path is at the window title.

The name "ser" now contains the serial, which we can read this way:

ser.readline()

That prints whatever the serial is saying.

The FreeCAD part:

We need a function that changes the position of the servo arm to the input value. From the arduino, we receive a number from 0 to 1024, a servo rotates ~180 degrees, so a conversion is needed.

def SERVO(valor):
  angle = valor*-180.0/1024.0
  Position = FreeCAD.Placement(App.Vector(12.5,12,53),App.Vector(0,0,1),angle) 
  FreeCAD.ActiveDocument.Fillet004.Placement = Position

The first line of the function SERVO is the 1024 to 180º conversion, the second and third ones do the position change of the FreeCAD object. Note that the servo arm is called "Fillet004".

You can test this function by giving values to it, like SERVO(200) or SERVO(1000), it should move.

The link:

The function SERIAL links  everything together:

servalue0 = 0 # avoid problems with serial initialization

def SERIAL():
  global servalue0
  hysteresis = 6.0 # to smooth the movement
  try:
    servalue = int(ser.readline()) # to int the serial value
  except:
    servalue = servalue0
  if servalue > servalue0 + hysteresis or servalue < servalue0 - hysteresis:
    SERVO(servalue) # update the servo position
    servalue0 = servalue # keep last value to check hysteresis

The timer:

To give it life we need to call SERIAL function repetitively with a timer:

timer = QtCore.QTimer()
timer.timeout.connect(SERIAL)
timer.start(1)


First line creates timer object, second one connects its signals to the SERIAL function and the third one makes it emit a signal every 1 ms.


Complete Python script:

If you just want to test it, create the Arduino circuit, download the servo model and copy paste this at FreeCAD console:

"""
Javier Martinez Garcia, 2014 
"""
import serial
from PySide import QtCore

try:
  ser = serial.Serial('/dev/ttyACM0', 9600)
  
except:
  ser = serial.Serial('/dev/ttyACM1', 9600)

def SERVO(valor):
  angle = valor*-180.0/1024.0
  Position = FreeCAD.Placement(App.Vector(12.5,12,53),App.Vector(0,0,1),angle) 
  FreeCAD.ActiveDocument.Fillet004.Placement = Position 

servalue0 = 0 # avoid problems with serial initialization

def SERIAL():
  global servalue0
  hysteresis = 6.0 # to smooth the movement
  try:
    servalue = int(ser.readline()) # to int the serial value
  except:
    servalue = servalue0
  if servalue > servalue0 + hysteresis or servalue < servalue0 - hysteresis:
    SERVO(servalue) # update the servo position
    servalue0 = servalue # keep last value to check hysteresis

timer = QtCore.QTimer()
timer.timeout.connect(SERIAL)
timer.start(1)



And this is all you need to repeat my video.
There are things that can be improved, for example the timer. Maybe using the threading library can give the same result.

About the possibilities, the communication can be bi-directional too, here I show realworld->FreeCAD, but the opposite is perfectly possible. And the FreeCAD objects attributes that can be changed do not need to be exclusively placement, but color or even parametric models.

Just imagine.

Bye!