Brian Ekins

July 2009

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

July 01, 2009

Introduction to Attributes

This is the first of at least two posts on attributes.  This first one will cover the basics and subsequent postings will go into some more advanced aspects.  Most programs your write will likely only need the information covered in this post.

The attribute functionality of Inventor is something that’s only available through the API; it doesn’t have a user-interface.  The basic concept of attributes is very simple; it’s the ability to associate information with Inventor entities.  Where it get’s a little more complicated is in understanding when this functionality might apply to your programs and how you can take advantage of it.

First, let’s look at the API for attributes.  Every object that supports attributes has the AttributeSets property from which you can access the attributes associated with that particular object.  Below is the object model for attributes.
 AttributesModel

The “Inventor Entity” is any object that supports the AttributeSets property.  The AttributesSets object returned by this property is an object collection that lets you access all of the AttributeSet objects associated with the object, and it also lets you create new AttributeSet objects.  An AttributeSet is exactly what the name implies; a set of attributes.  They provide a way of grouping the attributes associated with an object.  Typically, each application that adds an attribute to an object will create their own attribute set so that their attributes aren’t mixed with another application’s attributes.

Creating an Attribute
An AttributeSet object is also a collection object and provides access to the Attribute objects it contains and provides the ability to create new attributes.  Below is an example VBA macro that demonstrates this by creating a new attribute set on a selected entity and then adds an attribute to that set.

Public Sub CreateAttribute()
    ' Get the active document.  It can be any type of document.
    Dim doc As Document
    Set doc = ThisApplication.ActiveDocument 

    ' Get the selected entity.
    Dim entity As Object
    On Error Resume Next
    Set entity = doc.SelectSet.Item(1)
    If Err Then
        MsgBox "You must select an entity."
        Exit Sub
    End If

    ' Get the AttributeSets object from the selected entity.
    Dim attribSets As AttributeSets
    Set attribSets = entity.AttributeSets
    If Err Then
        MsgBox "You must select an entity that supports attributes."
        Exit Sub
    End If
    On Error GoTo 0 

    ' Create an attribute set.
    Dim attribSet As AttributeSet
    Set attribSet = attribSets.Add("SampleSet")

    ' Create an attribute on the set.
    Dim attrib As Inventor.Attribute
    Set attrib = attribSet.Add("SampleAttribute", _
                               kStringType, "A Test")
End Sub

The first portion of the program gets the entity that is currently selected within the active document.  Notice that the variable “entity” is declared as Object, indicating that any type of object can be handled.  The error handling is used to catch the case where no entity has been selected.

The next section of the program gets the AttributeSets object from the selected entity.  Since the program allows you to select anything, the error handling is used to catch the case where the selected object doesn’t support the AttributeSets property.

Finally, we have an AttributeSets object that was obtained from the selected entity.  The next section creates an AttributeSet object using the Add method of the AttributeSets object.  It supplies the name to use for the AttributeSet.  The name must be unique with respect to the other AttributeSet objects associated with that particular AttributeSets object.  Typically you would append the set name with your company or application name to make it unique, i.e. “modmachineSampleTest”.

The final section of the program creates an attribute within the attribute set.  The creation of the attribute takes three arguments; name, type, and value.  The name must be unique with respect to the other attributes within this attribute set.  Since it’s likely you’re the only one adding attributes to your set this is easily managed by you.  Attributes can be one of four different types; Double, Integer, String, or array of Bytes.  In the example above it creates a String attribute with the value of “A Test”.

Getting an Existing Attribute From an Entity
Now that you’ve seen how to create an attribute on an entity, how do you get that attribute back at a later time?  There are two ways of accessing attributes.  The first uses the objects we’ve already looked at; AttributeSets and AttributeSet.  The sample code below illustrates getting the attribute that was just created.

Public Sub GetAttribute()
    ' Get the active document.  It can be any type of document.
    Dim doc As Document
    Set doc = ThisApplication.ActiveDocument

    ' Get the selected entity.
    Dim entity As Object
    On Error Resume Next
    Set entity = doc.SelectSet.Item(1)
    If Err Then
        MsgBox "You must select an entity."
        Exit Sub
    End If

    ' Get the AttributeSets object from the selected entity.
    Dim attribSets As AttributeSets
    Set attribSets = entity.AttributeSets
    If Err Then
        MsgBox "You must select an entity that supports attributes."
        Exit Sub
    End If
    On Error GoTo 0

    ' Check to see if it has the "SampleSet" attribute set.
    If attribSets.NameIsUsed("SampleSet") Then
        ' Get the attribute set.
        Dim attribSet As AttributeSet
        Set attribSet = attribSets.Item("SampleSet")

        ' Get the attribute on the set.
        Dim attrib As Inventor.Attribute
        Set attrib = attribSet.Item("SampleAttribute")
        MsgBox "Attribute " & attrib.name & " = " & attrib.Value
    Else
        MsgBox "The entity does not have the expected attribute set."
    End If
End Sub

Most of the code in the sample above is the same as the previous sample.  However, once it has the AttributeSets object from the selected entity it uses the NameIsUsed property of the AttributeSets object to see if there is an attribute set named “SampleSet”.  If there is then it gets the attribute named “SampleAttribute”.  Finally it displays the name and value of that attribute.

The approach demonstrated above illustrates how you can get an attribute from an known entity.  The problem with this approach is that in many cases you don’t have a reference to the entity the attribute is attached to.  For example, you might have attached an attribute to a face so you can use the attribute at a later time to find that face again.  Using the technique shown above you would need to iterate over every face in the model until you found the one that had the “SampleSet” attribute set.  Not a very efficient way to find the attribute.  Another approach of getting attributes is discussed below.

Getting an Existing Attribute Using the Attribute Manager
The other way to find existing attributes is to use the AttributeManager object.  This is an object you obtain using the AttributeManager property of the Document object.  The most common use of the attribute manager is to query for attributes that exist in the document.  For example, the code below will find all entities that have an attribute set called “SampleSet”.

Public Sub GetAttributeUsingManager()
    ' Get the active document.  It can be any type of document.
    Dim doc As Document
    Set doc = ThisApplication.ActiveDocument 

    ' Get the attribute manager.
    Dim attribMgr As AttributeManager
    Set attribMgr = doc.AttributeManager

    ' Find all attribute sets named "SampleSet".
    Dim foundEntities As ObjectCollection
    Set foundEntities = attribMgr.FindObjects("SampleSet")

    MsgBox "Found " & foundEntities.Count & " entities."
End Sub

You can see this is much easier, and faster, than looking through all of the entities to see if they have a particular attribute set.

There are three different Find methods to choose from and you’ll choose one over the other depending on which type of object will be the most convenient for you to use in your program.  The three find methods are FindAttributes, FindAttributeSets, and FindObjects.  All three of them take the same input arguments and do the same type of search with the only difference being what they return.  The FindAttributes method returns a list of Attribute objects as an AttributesEnumerator object.  The FindAttributeSets method returns a list of AttributeSet objects as an AttributeSetsEnumerator object.  The FindObjects returns a list of entities as an ObjectCollection object.

The three Find methods have three arguments.  The first is the name of the attribute set, the second is the name of the attribute, and the third is the value of the attribute.  You can use wild cards when searching.  For example if I had added the following attribute sets to several different entities, SampleSet1, SampleSet2, SampleSet3, and SampleSet4, the following query would find and return all four attribute sets.  Notice the asterisk wild card at the end of “SampleSet”

    ' Find all attribute sets named "SampleSet".
    Dim foundSets As AttributeSetsEnumerator 
    Set foundSets = attribMgr.FindAttributeSets("SampleSet*")

The following query will find all attribute sets named “SampleSet” that contain an attribute named “TableType”.

    ' Find all attribute sets named "SampleSet".
    Dim foundSets As AttributeSetsEnumerator 
    Set foundSets = attribMgr.FindAttributeSets("SampleSet", _
                                                "TableType")

This last one find all attribute sets that have an attribute whose value is “ACME Fastener”.

    ' Find all attribute sets named "SampleSet".
    Dim foundSets As AttributeSetsEnumerator 
    Set foundSets = attribMgr.FindAttributeSets("*", "*", _
                                                "ACME Fastener")

As discussed above, the various find methods return different types of objects.  Frequently you may have a reference to one type of object but also need the associated objects.  We’ve already seen how how you can walk down the object model from an entity to get an associated AttributeSet object and from the AttributeSet object to get the Attribute objects.  You can also walk up the object model.  If you start with an Attribute object you can use its Parent property to get the AttributeSet object it is associated with.  You can use the Parent property of the AttributeSet object to get the parent AttributeSets object.  And finally, you can use the AttributeSets object’s Parent property to get the entity the attribute set is associated with. 

Why Use Attributes?

A common question when first learning about attributes is “why/how would you use them?”.  Attributes don’t do anything by themselves but only serve as a tool to enable other things.  Because of this they are used in many different ways for many unique applications.  Here are a some examples that illustrate how attributes can be used.  They also demonstrate why you would choose one of the find methods of the AttributeManager object over another. 

Probably the most common use of attributes is for naming an entity so you can find it later.  For example, you might be automating the creation of an assembly and need to find the entities that will be used as input when creating constraints to position the parts in the assembly.  One approach is to add an attribute set to the entity so you can use one of the AttributeManager find method find it later.  In this case it’s the entity that’s of interest and the attribute is just serving as the mechanism to find the entity and using the FindObjects method makes sense because it directly returns the object so you can easily use it to create the constraint.

Another common example of the use of attributes is adding additional information to an entity that’s important for your application. For example you might write a program to create a custom table in a drawing.  In this case each row of the table represents something unique to your application is not something that Inventor is aware of.  You can use the API to create a custom table with the data you want to display but you can also add an attribute set to the custom table that contains additional information that will be useful to you to understand where the data in the table came from so you can easily update it.  In this case it’s probably the AttributeSets object that’s the most interesting since it gives you easy access to the attributes to allow you to query them to determine the table information.  Using the FindAttributeSets method makes more sense in this case.

Here’s an example where Inventor takes advantage of attributes.  I talked this in a previous post, http://modthemachine.typepad.com/my_weblog/2009/04/controlling-layers-in-your-flat-pattern-export.html.  In this example, you add attributes to the edges of a flat pattern and the translator will use the attributes to determine which layer to create the corresponding geometry on.

Another example is face colors.  You can right-click on a face in a part and select the Properties command to change the color of that face.  Internally, Inventor remembers the color you selected by adding an attribute to that face that has the name of the color.

April 29, 2009

DevLabs

Here’s an unbelievable opportunity, if you’re in a position that you can take advantage of it.  It’s essentially up to a weeks worth of free programming consultation.  Kean Walmsley, who is the manager of the DevTech team explains it fully in his blog Through the Interface.  DevTech is the support group for the Autodesk Developer Network (ADN).  Take a look at the locations and dates and see if one of them works for you.

April 21, 2009

Inventor 2010 API Information

The official Inventor API page has been updated with information about Inventor 2010, http://www.autodesk.com/developinventor.  Here are a couple of highlights of what’s available. 

  • A PowerPoint presentation, along with sample code, that discusses the API enhancements that are in Inventor 2010.  It was pointed out that the PowerPoint file is in the new pptx format.  You can download an Office compatibility pack so you can view the new file formats in older versions of office.  The compatibility pack is available from Microsoft here.
  • The new Object Model chart for Inventor 2010.  (This is also installed in the Docs directory as part of the Developer Tools in the SDK.)
  • There is an update for the SDK.  There were some issues discovered in the wizards and sample programs that have been resolved in this update.  It’s a complete SDK install so you’ll need to uninstall your existing User Tools or Developer Tools if you have installed them and then install these updates.

April 17, 2009

Extracting a Specific Size Icon From a .ico File

Here’s something that I recently discovered after a lot of searching and some experimenting.  In my add-ins I prefer to use .ico files for my button icons.  Icons have some advantages over .bmp files in that you can define a transparent background and you can save multiple images within a single .ico file.  This is convenient because to fully support Inventor’s interface you should have three icon sizes, as listed below.

16x16 pixels – Used for small icons in both Classic and Ribbon interfaces.
24x24 pixels – Used for large icons in Class interface.
32x32 pixels – Used for large icons in Ribbon interface.

Using an icon editor it’s fairly easy to create the icons of the various sizes and then use that .ico file in my program.  What I couldn’t figure out was how to extract an icon of a specific size from the .ico file.  My searches just lead to postings from other who had the same question but no answers.  I finally began experimenting and found something that works.

In my add-in project I add each of the .ico files as a resource to my project.  In VB.Net you can access any item in your resources by name.  The name of my icon in the example below is “MyCommand1”.  The code below reads the icon of the specified size from the resource and ends up holding a reference to an Icon object. 

Dim smallIcon As System.Drawing.Icon
smallIcon = New System.Drawing.Icon(My.Resources.MyCommand1, 16, 16)


The code below reads in the large icon file from the same icon resource. 

Dim largeIcon as Sytem.Drawing.Icon
largeIcon = New System.Drawing.Icon(My.Resources.MyCommand1, 32, 32)


You can’t directly use an Icon object with Inventor’s API since it is expecting an IPictureDisp object.  The code below demonstrates how to convert an Icon to an IPictureDisp.  You’ll need to reference the Microsoft.VisualBasic.Compatibility and stdole libraries into the project first.

Dim smallPic As stdole.IPictureDisp
smallPic = Microsoft.VisualBasic.Compatibility.VB6.IconToIPicture( _
                                                          smallIcon)

April 14, 2009

Controlling Layers in Your Flat Pattern Export

When writing out a flat pattern as DXF or DWG you have control over which layers various bits of the flat pattern are written to.  You can see these options in the Save Copy As dialog when you right-click on the flat pattern in the browser, as shown below.

SaveCopyAsFlatPattern


The dialog allows you to specify the layer name for various categories of items that can exist in a flat pattern.  You can change the layer names and control which groups are written to the DXF or DWG file.  This is all good functionality but you’re limited to the predefined sets of groups.  There is a solution to this that allows you to have very detailed control over which layer specific entities are written to.

Using the API you can assign an attribute to an edge in the flat pattern and this will control which layer that edge will be written to in the DXF or DWG file.  The name of the layer is specified by the attribute.  Below is a very simple example that will add an attribute to a selected edge.

' This program assumes that a sheet metal document is open, the
' flat pattern is active, and that an edge of the flat pattern is
' selected.  For simplicity, no error checking is done to
' validate this.
Sub AddLayerName()
    ' Get the active document.
    Dim oDoc As PartDocument
    Set oDoc = ThisApplication.ActiveDocument

    ' Get the selected edge.
    Dim oEdge As Edge
    Set oEdge = oDoc.SelectSet(1)

    ' Add an attribute set named “FlatPatternAttributes”
    ' to the selected edge.
    Dim attSet As AttributeSet
    Set attSet = oEdge.AttributeSets.Add("FlatPatternAttributes") 

    ' Add an attribute specifying the name of the layer.
    Dim myLayerName As String
    myLayerName = "CustomLayer"
    Call attSet.Add("LayerName", kStringType, myLayerName)
End Sub


Any edge on the flat pattern model is valid to name.  The bend and tangent lines are also Edge objects and can be assigned a layer name too.  Defining layers for sketches and sketch entities is not currently supported so attributes added to these will be ignored.  When you specify the layer of an entity using this technique it overrides the dialog settings.  For example, if an edge will normally be written to the IV_OUTER_PROFILE it will instead be written to the layer you specify using the attribute.

One other thing to be aware of is that only the edges that are visible are written to the DXF or DWG file.  The picture below shows a flat pattern as viewed with the Z axis pointed towards you.  This is the geometry you would expect in the output DXF or DWG file.

FlatPattern


Here’s a close-up of an area of the model where it’s slightly rotated.  The red selected edge is on the “back” of the part and will not be written out to the DXF or DWG file.  Adding an attribute to this edge won’t have any effect since the edge is not written out.  The same is true for the bend and tangent lines too.  Only those on the “front” of the part will be written out.

FlatPatternEdge


When edges have been attributed in this way the Save Copy As command honors this by displaying this as a layer in the dialog to allow you to choose whether to write it out or not.  Notice in the picture below that the last layer in the list is “CustomLayer” which is the name that was assigned using the macro above.

SaveCopyAsFlatPatternAfter 


These layer overrides are also honored when using the API to write out a DXF or DWG file of the flat pattern.  The program below is directly from Inventor’s online help and illustrates using the API to write out a DXF file of the flat pattern.  See the topic on the DataIO object in the online help for additional information about this.

Public Sub WriteSheetMetalDXF()
    ' Get the active document.  This assumes it is a part document.
    Dim oDoc As PartDocument
    Set oDoc = ThisApplication.ActiveDocument

    ' Get the DataIO object.
    Dim oDataIO As DataIO
    Set oDataIO = oDoc.ComponentDefinition.DataIO

    ' Build the string that defines the format of the DXF file.
    Dim sOut As String
    sOut = "FLAT PATTERN DXF?AcadVersion=R12"

    ' Create the DXF file.
    oDataIO.WriteDataToFile sOut, "C:\temp\flat.dxf"
End Sub


If attributes are something new to you watch this blog over the next couple of weeks for additional postings explaining attributes in more detail.

April 09, 2009

Positioning Assembly Occurrences

Rob Cohee recently posted a video demonstrating some skeletal modeling techniques in Inventor related to .  To simplify the process he used a VBA macro that I wrote for him.  It’s a simple macro and is useful by itself but also helps to demonstrate some API capabilities.  Here’s the macro that I provided Rob.

Public Sub AlignOccurrencesWithConstraints()
    Dim assemblydoc As AssemblyDocument
    Set assemblydoc = ThisApplication.ActiveDocument

    ' Get the occurrences in the select set.
    Dim occurrenceList As New Collection
    Dim entity As Object
    For Each entity In assemblydoc.SelectSet
        If TypeOf entity Is ComponentOccurrence Then
            occurrenceList.Add entity
        End If
    Next

    If occurrenceList.Count < 2 Then
        MsgBox "At least two occurrences must be selected."
        Exit Sub
    End If

    ' This assumes the first selected occurrence is the "base"
    ' and will constrain the base workplanes of all the other parts
    ' to the base workplanes of the first part. If there are
    ' constraints on the other they end up being over constrained.

    ' Get the planes from the base part and create proxies for them.
    Dim baseOccurrence As ComponentOccurrence
    Set baseOccurrence = occurrenceList.Item(1)

    Dim BaseXY As WorkPlane
    Dim BaseYZ As WorkPlane
    Dim BaseXZ As WorkPlane
    Call GetPlanes(baseOccurrence, BaseXY, BaseYZ, BaseXZ)

    Dim constraints As AssemblyConstraints
    Set constraints = assemblydoc.ComponentDefinition.constraints

    ' Iterate through the other occurrences
    Dim i As Integer
    For i = 2 To occurrenceList.Count
        Dim thisOcc As ComponentOccurrence
        Set thisOcc = occurrenceList.Item(i)

        ' Move it to the base occurrence so that if the base is
        ' not fully constrained it shouldn't move when the flush
        ' constraints are added.
        thisOcc.Transformation = baseOccurrence.Transformation

        ' Get the planes from the occurrence
        Dim occPlaneXY As WorkPlane
        Dim occPlaneYZ As WorkPlane
        Dim occPlaneXZ As WorkPlane
        Call GetPlanes(thisOcc, occPlaneXY, occPlaneYZ, occPlaneXZ)

        ' Add the flush constraints.
        Call constraints.AddFlushConstraint(BaseXY, occPlaneXY, 0)
        Call constraints.AddFlushConstraint(BaseYZ, occPlaneYZ, 0)
        Call constraints.AddFlushConstraint(BaseXZ, occPlaneXZ, 0)
    Next
End Sub

' Utility function used by the AlignOccurrencesWithConstraints macro.
' Given an occurrence it returns the base work planes that are in
' the part or assembly the occurrence references.  It gets the
' proxies for the planes since it needs the work planes in the
' context of the assembly and not in the part or assembly document
' where they actually exist.

Private Sub GetPlanes(ByVal Occurrence As ComponentOccurrence, _ 
                      ByRef BaseXY As WorkPlane, _ 
                      ByRef BaseYZ As WorkPlane, _ 
                      ByRef BaseXZ As WorkPlane)
    ' Get the work planes from the definition of the occurrence.
    ' These will be in the context of the part or subassembly, not 
    ' the top-level assembly, which is what we need to return.
    Set BaseXY = Occurrence.Definition.WorkPlanes.Item(3)
    Set BaseYZ = Occurrence.Definition.WorkPlanes.Item(1)
    Set BaseXZ = Occurrence.Definition.WorkPlanes.Item(2)

    ' Create proxies for these planes.  This will act as the work
    ' plane in the context of the top-level assembly.
    Call Occurrence.CreateGeometryProxy(BaseXY, BaseXY)
    Call Occurrence.CreateGeometryProxy(BaseYZ, BaseYZ)
    Call Occurrence.CreateGeometryProxy(BaseXZ, BaseXZ)
End Sub


The program above constrains the base work planes of the set of selected occurrences to the base work planes of one other selected occurrence (the base part).  It places three flush constraints between each part and the base part.  The advantage to this is that the occurrences are all constrained to the base part so that if the base part is repositioned all of the other parts will automatically move to maintain the relationship defined by the constraints.  The disadvantage is that it’s doing quite a bit of work and each constraint placement causes the assembly to recompute, which is what gave Rob time to go get coffee.

There’s another approach that is much faster if you just need to position parts but don’t need to associate their position to another part.  The macro below does this.  It’s simpler and runs much faster.  This macro positions the parts exactly as the previous macro but instead of creating constraints it just repositions them and then grounds them so they can’t be accidentally moved.

Public Sub AlignOccurrencesWithGround()
    Dim assemblydoc As AssemblyDocument
    Set assemblydoc = ThisApplication.ActiveDocument

    ' Get the occurrences in the select set.  The first occurrence
    ' selected will be the "base" where all of the other occurrences
    ' will be positioned relative to it.
    Dim baseOccurrence As ComponentOccurrence
    Dim baseTransform As Matrix
    Dim transObjects As TransientObjects
    Set transObjects = ThisApplication.TransientObjects
    Dim occList As ObjectCollection
    Set occList = transObjects.CreateObjectCollection
    Dim transformList As ObjectCollection
    Set transformList = transObjects.CreateObjectCollection
    Dim entity As Object
    For Each entity In assemblydoc.SelectSet
        ' Check that the selected entity is an occurrence.
        If TypeOf entity Is ComponentOccurrence Then
            ' Check if the base occurrence has been assigned.
            ' If not then this is the first occurrence found and
            ' use it as the base occurrence.
            If baseOccurrence Is Nothing Then
                Set baseOccurrence = entity
                Set baseTransform = baseOccurrence.Transformation
            Else
                ' This is another selected occurrence so just
                ' add it to the list.
                occList.Add entity
                transformList.Add baseTransform
            End If
        End If
    Next 

    ' Check that at least a base occurrence and one other occurrence
    ' was selected.
    If occList.Count < 1 Then
        MsgBox "At least two occurrences must be selected."
        Exit Sub
    End If

    ' Reposition all of the occurrences.  The TransformOccurrences
    ' method was new in Inventor 2009.
    Dim assemblyDef As AssemblyComponentDefinition
    Set assemblyDef = assemblydoc.ComponentDefinition
    Call assemblyDef.TransformOccurrences( occList, _
                                           transformList)

    ' Iterate through the occurrences and ground them.
    Dim i As Integer
    For i = 1 To occList.Count
        Dim thisOccurrence As ComponentOccurrence
        Set thisOccurrence = occList.Item(i)
        thisOccurrence.Grounded = True
    Next
End Sub


Here’s one final version of this macro that is still even simpler.  In the two previous macros the selected occurrences were moved relative to the location of the first selected occurrence.  The macro below moves every occurrence to be positioned relative to the base assembly coordinate system.  It doesn’t rely on selection but repositions every occurrence and grounds it.

Public Sub AlignOccurrencesWithOrigin()
    Dim assemblyDoc As AssemblyDocument
    Set assemblyDoc = ThisApplication.ActiveDocument
    Dim assemblyDef As AssemblyComponentDefinition
    Set assemblyDef = assemblyDoc.ComponentDefinition

    ' Create a matrix.  It is initialized as an identity matrix
    ' which means it defines a position as the origin and aligned
    ' with the global x, y, and z axes.
    Dim transGeom As TransientGeometry
    Set transGeom = ThisApplication.TransientGeometry
    Dim baseTransform As Matrix
    Set baseTransform = transGeom.CreateMatrix

    ' Create collections to load the occurrences into.
    Dim transObjects As TransientObjects
    Set transObjects = ThisApplication.TransientObjects
    Dim occList As ObjectCollection
    Set occList = transObjects.CreateObjectCollection
    Dim transformList As ObjectCollection
    Set transformList = transObjects.CreateObjectCollection

    ' Iterate through all of the occurrences.
    Dim occurrence As ComponentOccurrence
    For Each occurrence In assemblyDef.Occurrences
        ' Add each occurrence to the list.
        occList.Add occurrence

        ' Add the transform to the list.
        transformList.Add baseTransform

        ' Ground each occurrence.  This is ok to do here
        ' because the move will ignore the ground condition.
        occurrence.Grounded = True
    Next

    ' Reposition all of the occurrences.  The TransformOccurrences
    ' method was new in Inventor 2009. 
    Set assemblyDef = assemblydoc.ComponentDefinition
    Call assemblyDef.TransformOccurrences(occList, _
                                           transformList)
End Sub


Of course these macros just the demonstrate one solution using the API to a specific problem.  The same functionality demonstrated above and combined with other API functions can be used in many other ways to solve other problems.  All I’m trying to say is that if the macros above don’t quite do what you need them to do it’s time to start investigating Inventor’s API so that you can modify them to solve your specific problems.

March 30, 2009

Running Commands Using the API

Occasionally there is a need to run an Inventor command from a program.  There are a couple of reasons why you might want to do this.  One is when the API doesn’t support some functionality but Inventor does.  By running the command you can sometimes work around the API deficiency.  Another reason is when you want to take advantage of the interactive behavior of a command by starting a command and then turning it over to the user finish it.

Running a Command in Inventor
It turns out that it’s fairly simple to run any command in Inventor.  This includes Inventor’s standard commands and also Add-In commands.  In the API, every command is represented as a ControlDefinition object.  The ControlDefinition object supports the Execute method which is the equivalent of clicking on a button and running a command in the user-interface.  Below is an example that executes the Line command in a sketch.

Public Sub RunLineCommand()
    ' Get the CommandManager object.
    Dim oCommandMgr As CommandManager
    Set oCommandMgr = ThisApplication.CommandManager

    ' Get control definition for the line command.
    Dim oControlDef As ControlDefinition
    Set oControlDef = oCommandMgr.ControlDefinitions.Item( _
                                                 "SketchLineCmd") 
    ' Execute the command.
    Call oControlDef.Execute
End Sub

 

Getting Command Names
The tricky part in using the code above is knowing what the name of the command is that you want to run.  In the example above it uses “SketchLineCmd” to specify the Line command.  Every control definition has a name that uniquely identifies it.  There are a couple of techniques you can use to find out the name of the desired command.  The first is to use a program to create a list of all of the available commands.  The program below, (which is straight from Inventor’s programming help), does this.  It will create a file named “C:\Temp\CommandNames.txt” that contains a list showing the name and description of every command.  You can open this file using Notepad or another text editor and search for the command you’re interested in.  Command names are typically very similar to the command name you see in the user-interface.

Sub PrintCommandNames()
    ' Get the CommandManager object.
    Dim oCommandMgr As CommandManager
    Set oCommandMgr = ThisApplication.CommandManager
   
    ' Get the collection of control definitions.
    Dim oControlDefs As ControlDefinitions
    Set oControlDefs = oCommandMgr.ControlDefinitions

    ' Open the file and print out a header line.
    Dim oControlDef As ControlDefinition
    Open "C:\temp\CommandNames.txt" For Output As #1
    Print #1, Tab(10); "Command Name"; Tab(75); _
              "Description"; vbNewLine

    ' Iterate through the controls and write out the name.
    For Each oControlDef In oControlDefs
        Print #1, oControlDef.InternalName; Tab(55); _
                  oControlDef.DescriptionText
    Next

    ' Close the file.
    Close #1
End Sub


Another technique to find a command name is to use the Event Watcher utility that’s delivered as part of Inventor’s SDK.  This utility lets you listen to the various events that Inventor supports through the API.  One of these events is sent whenever a command is executed within Inventor and as part of that event it provides the name of the command that was executed.  To use this follow the steps below.

  1. Have Inventor running and in a state where the command you want to use is available.  For example, if you want to know the name of the Circle command you will need to have a sketch active.
  2. Run EventWatcher.exe.  On a Windows Vista install this is typically at:

    C:\Users\Public\Documents\Autodesk\Inventor 2009\SDK\DeveloperTools\Tools\EventWatcher\bin\Release

    On a Windows XP install it is typically at:

    C:\Program Files\Autodesk\Inventor 2009\SDK\DeveloperTools\Tools\EventWatcher\bin\Release

    If you can’t find it, it’s likely you haven’t installed the developer tools portion of the SDK.  To do that install DeveloperTools.msi, which is in the SDK directory.  If you’re running Vista you’ll need to disable UAC to successfully install the developer tools.
  3. In EventWatcher, find the event UserInputEvents.OnActivateCommand and make sure the event has a check mark beside it.
  4. Run the whatever command in Inventor that you want to find the name for.
  5. Get the command name from EventWatcher.  The picture below shows the EventWatcher interface with the OnActivateCommand selected and the results after running the New Sketch and Sketch Line commands.

EventWatcher

 

Limitations
Being able to run any command using the API may seem like the answer to the problem where the API doesn’t cover all of Inventor’s functionality.  Unfortunately this isn’t a solution to that problem.  When you execute a command using the functionality discussed above it is exactly the same as if you had clicked on the command in the user-interface; the command is started.  The problem is that most commands need additional input from the user.  For example, the Extrude command displays a dialog to get the required input from the user or the Arrange Dimensions command doesn’t have a dialog but does require the user to select the set of dimensions to arrange.

In general, when running a command using the API you can’t provide the input the command is expecting so you can’t fully control and use the command via the API.  There are a couple of exceptions to this discussed below but for most commands you can only start them through the API.  This is still useful because there are cases where you only want to start a command and then let the user take over.

Providing Input to Commands
As mentioned above, there are some exceptions about providing input to commands that will allow you to better utilize them from the API.  There are two techniques of providing input to a command.  The first is pre-selecting the entities that the command should process.  This will only work for a small set of commands but is a workaround in a few cases where the API doesn’t directly support the equivalent of the command functionality.

An example of the use of pre-selecting entities is the new Arrange Dimensions command in Inventor 2010.  The API doesn’t support this functionality yet but by using this pre-selection technique you can use the command directly.  You select entities by adding them to the SelectSet object.  This is the equivalent of the user selecting them.  Many commands will check to see if valid entities are selected and operate on them if they are.  The program below takes advantage of this. 

Public Sub ArrangeDimensions()
    ' Get the active document, assuming it is a drawing.
    Dim oDrawDoc As DrawingDocument
    Set oDrawDoc = ThisApplication.ActiveDocument

    ' Get the collection of dimensions on the active sheet.
    Dim oDimensions As DrawingDimensions
    Set oDimensions = oDrawDoc.ActiveSheet.DrawingDimensions

    ' Get a reference to the select set and clear it.
    Dim oSelectSet As SelectSet
    Set oSelectSet = oDrawDoc.SelectSet
    oSelectSet.Clear

    ' Add each dimension to the select set to select them.
    Dim oDrawDim As DrawingDimension
    For Each oDrawDim In oDimensions
        oSelectSet.Select oDrawDim
    Next

   ' Get the CommandManager object.
    Dim oCommandMgr As CommandManager
    Set oCommandMgr = ThisApplication.CommandManager

    ' Get control definition for the arrange dimensions command.
    Dim oControlDef As ControlDefinition
    Set oControlDef = oCommandMgr.ControlDefinitions.Item( _ 
                                "DrawingArrangeDimensionsCmd")

    ' Execute the command.
    Call oControlDef.Execute
End Sub

 

In the case of the Arrange Dimensions sample above, it fully completes the command.  That is when you run the macro, all of the dimensions on the active sheet will be re-arranged.  That’s because if the command sees some dimensions in the select set it operates on them without asking for any more input.  Most commands, even if they support pre-selection of entities, will still allow additional selection so they will still require user input.  For example the Fillet command will take any pre-selected edges as input but still displays the dialog to allow the user to select more edges and specify any other options.

There is another way of providing input for most commands expect a filename as input.  Through the API you can predefine a filename and then run the command.  If a filename is defined, the command will use it.  For some commands, where only a filename is needed, it will skip the dialog portion of the command, for example the Place Component command.  Where more information is required it will use the predefined filename as the default filename and the command will run as normal by displaying its dialog so the user can change the filename and/or set other options.  The Base View command that’s used to create a drawing view is a good example of this.

You predefine a filename using the PostPrivateEvent method of the CommandManager object.  The example below demonstrates its use with the Place Component command.  When the macro executes the Place Component command checks to see if a filename has already been defined.  If it has it uses that filename and skips the dialog step and continues to the step where the user can specify the placement point of the component.  This is a good example of defining the inputs and then allowing the command to take over so you can take advantage of the interactivity the command provides.

Public Sub PlacePart()
    ' Get the command manager.
    Dim oCommandMgr As CommandManager
    Set oCommandMgr = ThisApplication.CommandManager

    ' Post the filename.
    Call oCommandMgr.PostPrivateEvent(kFileNameEvent, _
         "C:\Users\ekinsb\Documents\Inventor 2010\Showcasetest.ipt")

    ' Get control definition for the place component command.
    Dim oControlDef As ControlDefinition
    Set oControlDef = oCommandMgr.ControlDefinitions.Item( _ 
                                    "AssemblyPlaceComponentCmd") 

    ' Execute the command.
    Call oControlDef.Execute
End Sub

 

Command Execution Behavior

In all of the examples above the Execute method has been used to run the desired command.  The behavior of this method is to tell Inventor to run the command but immediately return control back to your program.  In the example of placing a component in the assembly, control returns back to the macro right after calling Execute, event though the user is still in the middle of placing components.  In many cases this is ok because it’s likely this is the last step in your program, but sometimes you may want to wait until the command has been completed and then finish some additional steps in your program.  You can do this using the Execute2 method.  This method has a single argument indicating if you want to run the command synchronously or asynchronously.  Asynchronous is the same as the Execute method and will immediately return control back to your program.  Synchronous won’t return control back to your program until the command has finished processing. 

Here’s a variation of the previous program that reports on which components were just placed.

Public Sub PlacePart2()
    ' Get the assembly component definition.
    Dim oAsmDef As AssemblyComponentDefinition
    Set oAsmDef = ThisApplication.ActiveDocument.ComponentDefinition

    ' Get the command manager.
    Dim oCommandMgr As CommandManager
    Set oCommandMgr = ThisApplication.CommandManager

    ' Post the filename.
    Call oCommandMgr.PostPrivateEvent(kFileNameEvent, _
          "C:\Users\ekinsb\Documents\Inventor 2010\Showcasetest.ipt")

    ' Get control definition for the place component command.
    Dim oControlDef As ControlDefinition
    Set oControlDef = oCommandMgr.ControlDefinitions.Item( _
                                 "AssemblyPlaceComponentCmd")

    ' Get the current number of occurrences.
    Dim iOccurrenceCount As Long
    iOccurrenceCount = oAsmDef.Occurrences.Count 

    ' Execute the command synchronously. 
    Call oControlDef.Execute2(True)

    ' Display information about the placed components.
    Dim strPrompt As String
    strPrompt = oAsmDef.Occurrences.Count - iOccurrenceCount & _
                           " occurrences were placed:" & vbCr
    Dim i As Integer
    For i = iOccurrenceCount + 1 To oAsmDef.Occurrences.Count
        strPrompt = strPrompt & "    " & _
                    oAsmDef.Occurrences.Item(i).Name & vbCr
    Next
    MsgBox strPrompt
End Sub

March 12, 2009

Combining Multiple Actions into a Single Undo

A question was recently asked in the customization newsgroup about how to combine several actions into one undo.  I think this topic is of general interest and a good one to discuss here.  I think this is a capability that's typically overlooked but is something that's easy to add to your existing macros.  Below is an example of a simple macro that draws a triangle on the active sketch.

Public Sub DrawTriangle()
    ' Check to see that a sketch is active.
    If Not TypeOf ThisApplication.ActiveEditObject Is sketch Then
        MsgBox "A sketch must be active."
        Exit Sub
    End If

    ' Get a reference to the sketch.
    Dim oSketch As sketch
    Set oSketch = ThisApplication.ActiveEditObject

    Dim oTG As TransientGeometry
    Set oTG = ThisApplication.TransientGeometry

    ' Draw the three lines of the triangle.
    Dim aoLines(2) As SketchLine
    Set aoLines(0) = oSketch.SketchLines.AddByTwoPoints( _
                              oTG.CreatePoint2d(0, 0), _
                              oTG.CreatePoint2d(5, 0))
    Set aoLines(1) = oSketch.SketchLines.AddByTwoPoints( _
                              aoLines(0).EndSketchPoint, _
                              oTG.CreatePoint2d(2.5, 4))
    Set aoLines(2) = oSketch.SketchLines.AddByTwoPoints( _
                              aoLines(1).EndSketchPoint, _
                              aoLines(0).StartSketchPoint)

    ' Add a horizontal constraint to the bottom line.
    Call oSketch.GeometricConstraints.AddHorizontal(aoLines(0))
End Sub


If you run this program it will create a triangle.  If you want to get rid of the triangle you'll need to do 4 undo steps, one for the horizontal constraint and three for the lines.  Since from the users perspective this was all created as a single step it would be nice if it would undo using a single undo.

Transaction Basics
You can combine multiple actions into a single undo using transactions.  A transaction is any action that can be undone.  Many, if not most, API calls are not transacted.  In the example above, checking that a sketch is active, getting the sketch, and getting the TransientGeometry are not transacted since they don't cause any change to the current document.  Creating the lines and the horizontal constraint are automatically transacted.  If you don't do anything special, each API call that causes a transaction will show up in the undo list as an individual item and can be undone.

To combine a set of actions into a single undo the API allows you to create your own transaction which wraps the other transactions.  Since the other transactions are nested within your transaction they aren't visible in the undo command and are undone and redone as a group whenever your transaction is undone or redone.  The functionality to use transactions through the API is supported through the TransactionManager object.  The modified version of the previous program shown below demonstrates this.

Public Sub DrawTriangle()
    ' Check to see that a sketch is active.
    If Not TypeOf ThisApplication.ActiveEditObject Is sketch Then
        MsgBox "A sketch must be active."
        Exit Sub
    End If

    ' Get a reference to the sketch.
    Dim oSketch As sketch
    Set oSketch = ThisApplication.ActiveEditObject

    Dim oTG As TransientGeometry
    Set oTG = ThisApplication.TransientGeometry

    ' Create a transaction.
    Dim oTransMgr As TransactionManager
    Set oTransMgr = ThisApplication.TransactionManager
    Dim oTrans As Transaction
    Set oTrans = oTransMgr.StartTransaction( _ 
                 ThisApplication.ActiveDocument, "Create Triangle")

    ' Draw the three lines of the triangle.
    Dim aoLines(2) As SketchLine
    Set aoLines(0) = oSketch.SketchLines.AddByTwoPoints( _ 
                                oTG.CreatePoint2d0,0),
                                oTG.CreatePoint2d(5, 0))
    Set aoLines(1) = oSketch.SketchLines.AddByTwoPoints( _
                                aoLines(0).EndSketchPoint,
                                oTG.CreatePoint2d(2.5, 4))
    Set aoLines(2) = oSketch.SketchLines.AddByTwoPoints( _
                                aoLines(1).EndSketchPoint,
                                aoLines(0).StartSketchPoint) 

    ' Add a horizontal constraint to the bottom line.
    Call oSketch.GeometricConstraints.AddHorizontal(aoLines(0))

    ' End the transaction.
    oTrans.End
End Sub


There are four extra lines from the previous program that are highlighted in red.  After running this program and looking in the Edit menu you'll see that you can undo "Create Triangle", as shown in the picture below. 

UndoInEditMenu

 

For many programs these four extra lines are all that you'll need to implement undo behavior.  The StartTransaction method creates and starts your transaction.  The first argument is a document and can be any document, it doesn't matter which one.  The second argument is the text that will appear in the Edit menu and can be whatever string you want to describe the group of steps being wrapped by this transaction.  The API calls after the StartTransaction and before the call of the End method will be nested within your transaction.

Handling Errors
The code above does demonstrate the minimum to wrap multiple actions into a single undo.  The only problem with it is if something happens in the program so that the Transaction.End method is never called.  For example, if an error is encountered when creating the lines.  In this case your transaction is started, some other transactions are created as a result of the API calls and are nested within your transaction but then it terminates if an error occurs, which leaves Inventor in a bit of a weird state.  To make sure this doesn't happen it's best to correctly handle any errors that might occur.  The modified example code below illustrates this. 

Public Sub DrawTriangle()
    ' Enable error handling.
    On Error GoTo ErrorFound

    ' Check to see that a sketch is active.
    If Not TypeOf ThisApplication.ActiveEditObject Is sketch Then
        MsgBox "A sketch must be active."
        Exit Sub
    End If

    ' Get a reference to the sketch.
    Dim oSketch As sketch
    Set oSketch = ThisApplication.ActiveEditObject

    Dim oTG As TransientGeometry
    Set oTG = ThisApplication.TransientGeometry

    ' Create a transaction.
    Dim oTransMgr As TransactionManager
    Set oTransMgr = ThisApplication.TransactionManager
    Dim oTrans As Transaction
    Set oTrans = oTransMgr.StartTransaction( _ 
                 ThisApplication.ActiveDocument, "Create Triangle")

    ' Draw the three lines of the triangle.
    Dim aoLines(2) As SketchLine
    Set aoLines(0) = oSketch.SketchLines.AddByTwoPoints( _ 
                                      oTG.CreatePoint2d(0, 0),
                                      oTG.CreatePoint2d(5, 0))
    Set aoLines(1) = oSketch.SketchLines.AddByTwoPoints( _ 
                                      aoLines(0).EndSketchPoint,
                                      oTG.CreatePoint2d(2.5, 4))
    Set aoLines(2) = oSketch.SketchLines.AddByTwoPoints( _ 
                                      aoLines(1).EndSketchPoint,
                                      aoLines(0).StartSketchPoint)

    ' Add a horizontal constraint to the bottom line.
    Call oSketch.GeometricConstraints.AddHorizontal(aoLines(0))

    ' End the transaction.
    oTrans.End

    ' No errors were found so exit before the error handling code.
    Exit Sub

' Handle any errors
ErrorFound:
    MsgBox "Unexpected error encountered."

    ' Check to see if a transaction was started.
    If Not oTrans Is Nothing Then
        ' Abort the transaction.
        oTrans.Abort
    End If
End Sub


The new code is highlighted.  At the beginning of the macro the On Error statement is used to turn on error handling.  In this case it's set up to jump to the ErrorFound: line if any error should occur.  In the ErrorFound: section it checks to see if a transaction object was created and if it was it calls the Transaction.Abort method.  This results in undoing every action since your transaction was created.  Instead of the Abort method the End method could have been called which will result in creating everything that was created up to the point of the error occurring.  The choice is yours whether to use Abort or End.  The reason this section checks to see if a transaction was created before call Abort is because an error could have occurred before the transaction was created and calling Abort or End would fail since your transaction object won't exist.

 

Transaction Issues to be Aware Of 
There are a couple of things to be aware of when using transactions through the API.  First, let's look at the behavior of Inventor relative to transactions and undo.  Try this:

  1. Create a new part document.
  2. Create another new part document.
  3. In one of the part documents (part 1), draw some lines in a sketch.
  4. Activate the other part document (part 2) and draw two lines.
  5. Activate part 1 and draw one line.
  6. Do an undo and the line will go away.
  7. Do another undo.  One of the lines in part 2 will go away.
  8. Close part 1.
  9. Do another undo.

There are two things to notice from the above steps.  First is that there is a single undo list for all of Inventor, not for each document.  The fact that you performed operations in different documents doesn't matter, all actions go onto a single undo list and are undone in the order they were performed.  The second issue is that in step 9 you are unable to do an undo because the undo command is unavailable.  This is because when a document is closed the undo stack is cleared.  This seems like a fairly big limitation but doesn't seem to matter in the real world since I've never found anyone that knew of this behavior before I showed it to them.

Because the undo stack is cleared when a document is closed you can't wrap the opening and closing of documents in a transaction.  That's probably the single biggest problem people have when using transactions.  There is one exception to this rule.  If the opening and closing of a document is done within your transaction, then it's ok.  For example, you could perform the following steps:

  1. Start a transaction.
  2. Open a part.
  3. Change a parameter.
  4. Update the part.
  5. Save a copy to a new file.
  6. Close the part.
  7. Insert the new part into the assembly.
  8. End the transaction.

Even though a document was closed, it didn't clear the undo stack since the opening and closing of the document was within your transaction.  If you perform an undo after the previous steps, the part will be removed from the assembly.  However, the file on disk will remain.

 

Other Functionality 
The TransactionManager supports a lot more functionality than I’ve described here.  However, I think the samples above demonstrate all that’s needed by 99.9% of the programs written.  There are descriptions of the additional functionality in the online help, if you’re interested.

You might also see references to something called a change processor.  This is a different mechanism for grouping multiple operations within a single undo but it’s much more complicated to use and cannot be used from within VBA.  I wouldn’t recommend it for most programs.  In fact I would only recommend it for larger application type of add-ins (like Tube & Pipe) that want to automate testing their custom commands.  The change processor provides important functionality to support that.

March 04, 2009

Accessing Assembly Components

A common need when working with an assembly is accessing the components that make up the assembly.  This reason to do this can be for a lot of reasons from creating your own custom BOM to editing parameter values within certain parts.  Here’s a quick overview of how to access its components.  (For this discussion I’m going to focus only on the parts and subassemblies within an assembly and not anything else that can be in an assembly; constraints, assembly features, level of detail, etc.)

What is an Assembly?
For the topic at hand, an assembly can be thought of as a container that contains instances of parts and other assembles.  Each instance (or occurrence) defines a position within the assembly and other properties that define the appearance and behavior (color, visibility, adaptive, etc.) of the instance.  An instance doesn’t contain any geometry but displays the geometry that it gets from the referenced part.  A part can be referenced once and then instanced many times by displaying that single part in different locations in the assembly.  Below is the example assembly I’ll use to illustrate.

AssemblyStructureAssembly


Below is the browser for this assembly.  You can see that the assembly consists of 14 occurrences where two of these are subassemblies and 12 of them are parts.  There is one unique subassembly and 5 unique parts, which is illustrated in the view of Windows Explorer shown below.  The geometry for the parts only exists within the five .ipt files.  The assembly references these parts and displays their graphics at positions defined by the location of the occurrence that represents the part.

AssemblyStructureBrowser

Here are some API terms you should be familiar with for the rest of the discussion:

ComponentOccurrence – This is the API object that represents an occurrence within an assembly.  A ComponentOccurrence can represent a subassembly or part and can be at any level of the assembly.  The example assembly above is represented by 14 ComponentOccurrence API objects.

File ReferenceThis is the reference between the assembly to the subassemblies and parts it contains.  In the example above there are six file references within the assembly to the various subassemblies and parts that are referenced.  There is only ever one file reference for a particular file.  For example, even though there are four Pillar parts, there is only one file reference between the assembly and the Pillar.ipt file.  For more information about file references see the previous post Understanding File References.

Leaf Occurrences – These are occurrences that define the end of each branch as you traverse an assembly.  In Inventor all parts are considered leaf occurrences.

There are a few ways to access the occurrences of an assembly.  Which one to use depends on what you’ll be doing with the results.  Below are descriptions and samples illustrating each one.

Assembly Structure
The first method is to access the full assembly structure.  This provides all of the occurrences and the full assembly tree, just as you see it in the browser.  This provides the complete view of the assembly and should be a sufficient amount of information for any task that needs the complete structure. 

Writing a program to access the full assembly tree is the most difficult of the approaches discussed here.  This is because an assembly tree is not a pre-defined size.  An assembly can have any number of levels with each level having any number of occurrences.  It’s a tree with any number of branches.  To get the full assembly structure you need to walk down every branch until you reach every leaf.  To handle a problem like this you need to use a recursive function, which is demonstrated in the code below.

Public Sub TraverseAssemblySample()
    ' Get the active assembly.
    Dim oAsmDoc As AssemblyDocument
    Set oAsmDoc = ThisApplication.ActiveDocument
    Debug.Print oAsmDoc.DisplayName

    ' Call the function that does the recursion.
    Call TraverseAssembly(oAsmDoc.ComponentDefinition.Occurrences, 1)
End Sub

Private Sub TraverseAssembly(Occurrences As ComponentOccurrences, _
                             Level As Integer)
    ' Iterate through all of the occurrence in this collection.  This
    ' represents the occurrences at the top level of an assembly.
    Dim oOcc As ComponentOccurrence
    For Each oOcc In Occurrences
        ' Print the name of the current occurrence.
        Debug.Print Space(Level * 3) & oOcc.Name

        ' Check to see if this occurrence represents a subassembly
        ' and recursively call this function to traverse through it.
        If oOcc.DefinitionDocumentType = kAssemblyDocumentObject Then
            Call TraverseAssembly(oOcc.SubOccurrences, Level + 1)
        End If
    Next
End Sub


The TraverseAssemblySample sub gets the active assembly document and calls the TraverseAssembly sub, passing in the collection of occurrences from the top-level assembly.  The Occurrences property of the AssemblyComponentDefinition object and the SubOccurrences property of the ComponentOccurrence object only return the occurrences directly within that assembly, not in any of its subassemblies.  The TraverseAssembly Sub then does all the work to walk the tree.  It iterates through each occurrences in the collection and for this sample,  prints out the name of each occurrence, but anything could be done with the occurrence at that point.  The next thing it does is what’s the most interesting; it checks to see if the occurrence is a subassembly or a part and if it is a subassembly it calls the TraverseAssembly sub again, passing the collection of occurrences in that subassembly.  The fact that the sub is calling itself is recursion.  It continues this process until it’s completely walked the assembly tree.

When running the program using the previous sample assembly, the following is written into the VBA Immediate window.  You can compare this with the browser view of the assembly and see that they match.

Sample.iam
   Floor:1
   Arch:1
      CurvedSupport:1
      CurvedSupport:2
      ArchTop:1
   Arch:2
      CurvedSupport:1
      CurvedSupport:2
      ArchTop:1
   Pillar:1
   Pillar:2
   Pillar:3
   Pillar:4
   Inventor:1

There are a couple of things to point out in the program.  One is the “Level” argument of the TraverseAssembly sub.  This is used to keep track of what level of the assembly the program is currently at so it can correctly indent the output of the tree and is not required to walk an assembly tree.  The area of the code that you would replace to do the work you want to perform with each occurrence is the line where it prints out the occurrence name.  This line is executed for every occurrence in the assembly.  You can replace this with code that performs whatever functionality you need to accomplish.  For example, if you need to create a custom BOM your code might access the document that’s referenced by this occurrence and extract some iProperty values and then print this out as part of a structured BOM.

Occurrence Lists 
For several releases of Inventor the only way to access the assembly through the API was to traverse the entire tree, as shown above.  Often you don’t need the tree structure but just want a simple list of the contents of the assembly.  There are a couple of properties that provide this simpler access to the assembly contents.

All Leaf Occurrences
This first example uses the AllLeafOccurrences property of the ComponentOccurrences object which returns a collection that contains all of the leaf occurrences of an entire assembly.  the leaf occurrences at every level of the assembly are returned in this single collection.  Remember that the leaf occurrences are the parts, so this won’t contain any subassembly occurrences.  The AllLeafOccurrences property also has one optional argument, (that isn’t used in the sample below), that provides some additional flexibility by allowing you to get only the leaf occurrences for a specified subassembly.

Public Sub GetPartOccurrences()
    ' Get the active assembly.
    Dim oAsmDoc As AssemblyDocument
    Set oAsmDoc = ThisApplication.ActiveDocument

    ' Get the assembly component definition.
    Dim oAsmDef As AssemblyComponentDefinition
    Set oAsmDef = oAsmDoc.ComponentDefinition

    ' Get all of the leaf occurrences of the assembly.
    Dim oLeafOccs As ComponentOccurrencesEnumerator
    Set oLeafOccs = oAsmDef.Occurrences.AllLeafOccurrences

    ' Iterate through the occurrences and print the name.
    Dim oOcc As ComponentOccurrence
    For Each oOcc In oLeafOccs
        Debug.Print oOcc.Name
    Next
End Sub


When the above program is run on the sample assembly the following is written to the VBA Immediate window.  Making the single call is much simpler than traversing the entire assembly looking for part occurrences.

Floor:1
CurvedSupport:1
CurvedSupport:2
ArchTop:1
CurvedSupport:1
CurvedSupport:2
ArchTop:1
Pillar:1
Pillar:2
Pillar:3
Pillar:4
Inventor:1


All Occurrences of a Specified Component
You can also get all of the occurrences that reference a specific document.  For example, you might need to access every instance of a specific fastener in an assembly to replace it with another.  To find every occurrence that references a specified document you can use the AllReferencedOccurrences property of the ComponentOccurrences object.  This property has a single required argument that specifies which part or assembly you want the referencing occurrences for.  It will return all of the occurrences at all levels of the assembly that reference the specified document.  The sample below illustrates this.

Public Sub FindOccurrences()
    ' Get the active assembly.
    Dim oAsmDoc As AssemblyDocument
    Set oAsmDoc = ThisApplication.ActiveDocument

    ' Get the definition of the assembly.
    Dim oAsmDef As AssemblyComponentDefinition
    Set oAsmDef = oAsmDoc.ComponentDefinition

    ' Get the document to find occurrences for. Since it’s assumed
    ' there is at least one occurrence in the assembly that 
    ' references this document, it will already be open since it
    ' was opened when the assembly was opened.
    Dim oDoc As Document
    Set oDoc = ThisApplication.Documents.ItemByName( _
                                "C:\AssemblySample\Pillar.ipt")

    ' Get the occurrences that represent this document.
    Dim oOccs As ComponentOccurrencesEnumerator
    Set oOccs = oAsmDef.Occurrences.AllReferencedOccurrences(oDoc)

    ' Print the occurrences to the Immediate window.
    Dim oOcc As ComponentOccurrence
    For Each oOcc In oOccs
        Debug.Print oOcc.Name
    Next
End Sub


The output for this program is shown below.

Pillar:1
Pillar:2
Pillar:3
Pillar:4


Getting a single list of specific occurrences is easier than traversing through the assembly but you also lose the assembly context that you get when traversing.  However, there are a few properties supported by the occurrence that provide this information in various ways.  If you ever need this kind of information look at the ContainingOccurrence, OccurrencePath, and ParentOccurrence properties of the ComponentOccurrence object.

All Referenced Documents
Another common need when working with an assembly is to access the unique documents referenced by the assembly, because in many cases you don’t need individual access to each occurrence.  For example, let’s say you want to change the material of all parts to a certain material.  You can do that with either one of the two previous programs by adding code that takes a given occurrence, gets the associated document, and changes its material.  The problem is that you’ll end up doing a lot more work than is necessary.  For example, in the sample assembly you’ll end up setting the material for the pillar part four times since it’s used that many times in the assembly, where you really only need to set it once for Pillar.ipt.

You can do this using the list of references instead of the occurrences since there is only one reference to each document regardless of how many times it’s used in the assembly.  The AllReferencedDocuments property of the AssemblyDocument object provides this.  It returns the complete set of referenced documents for the entire assembly regardless of the level in the assembly the reference actually occurs.

Here’s a simple example that illustrates this.

Public Sub ShowReferences()
    ' Get the active assembly.
    Dim oAsmDoc As AssemblyDocument
    Set oAsmDoc = ThisApplication.ActiveDocument

    ' Get all of the referenced documents.
    Dim oRefDocs As DocumentsEnumerator
    Set oRefDocs = oAsmDoc.AllReferencedDocuments

    ' Iterate through the list of documents.
    Dim oRefDoc As Document
    For Each oRefDoc In oRefDocs
        Debug.Print oRefDoc.DisplayName
    Next
End Sub


And here are the results when this sample is run.

Pillar.ipt
Inventor.ipt
ArchTop.ipt
CurvedSupport.ipt
Arch.iam
Floor.ipt


Here’s a more complex sample that uses this to demonstrate setting the material of every referenced part to Gold.

Public Sub ChangeAllPartMaterial()
    ' Get the active assembly document.
    Dim oAsmDoc As AssemblyDocument 
    Set oAsmDoc = ThisApplication.ActiveDocument

    ' Get the material to assign to all of the parts.
    Dim strMaterialName As String
    strMaterialName = "Gold"

    Dim oMaterial As Material
    On Error Resume Next
    ' Try to get the material of the specified name.
    Set oMaterial = oAsmDoc.Materials.Item(strMaterialName)
    If Err Then
        MsgBox "The material """ & strMaterialName & _
                     """ was not found in the library."
        Exit Sub
    End If
    On Error GoTo 0

    ' Iterate through all of the documents refrenced by the assembly.
    Dim oDoc As Document
    For Each oDoc In oAsmDoc.AllReferencedDocuments
        ' Check to see if this is a part.
        If oDoc.DocumentType = kPartDocumentObject Then
            Dim oPartDoc As PartDocument
            Set oPartDoc = oDoc

            ' Set the material.
            oPartDoc.ComponentDefinition.Material = oMaterial
        End If
    Next
End Sub

February 10, 2009

Setting the Weight for iPart Members

There was a question posted on the customization newsgroup that I spent a little time investigating and think the answer might be of general interest.  The objective is to have a column in the iPart table that has the weight for that member.  I wasn’t able to figure out a way to do this with built-in functionality in Inventor but it turned out it isn’t too difficult to write a program to accomplish the task.  Although the program and description below is specific to the weight of a part, it could be modified to handle other part information that you want to associate with an iPart member.

First, let’s look at the iPart factory to see what needs to exist for the program to function correctly.  The only thing special is that I created a custom iProperty called “Weight” and added that as one of the iProperties that I want to set from the table.  It didn’t have to be an iProperty and could have been an “Other” value but using an iProperty provides the most flexibility for using the value elsewhere in Inventor.  For example, in the the drawing.  The picture below shows the iPart table after setting it up with this iProperty.  The values for this new column are empty.

iPartTable


Now we need to assign the correct weight for each row to the weight column.  If you had to this manually you would:

  1. Activate a member.
  2. Run the iProperties command
  3. Go to the “Physical” tab of the iProperties dialog.
  4. Click the “Update” button on the “Physical” tab.
  5. Select and Copy the “Mass” field.
  6. Open the iPart table and paste the mass value into the table for the correct row.
  7. Close the iPart table.
  8. Repeat steps 1 to 7 for every row in the table.

As you can see this is very tedious and prone to errors.  Instead of manually going through these steps they can be automated using Inventor’s programming interface.  Below is a VBA macro that will do the work.

Public Sub UpdateWeightOfiParts()
    ‘ Get the active document.  This assumes it is a part.
    Dim oPartDoc As PartDocument
    Set oPartDoc = ThisApplication.ActiveDocument

    ' Check that this part is an iPart factory.
    If Not oPartDoc.ComponentDefinition.IsiPartFactory Then
        MsgBox "This part must be an iPart factory."
        Exit Sub
    End If

    Dim oFactory As iPartFactory
    Set oFactory = oPartDoc.ComponentDefinition.iPartFactory

    ' Check that there's a "Weight" column in the iPart table,
    ' and get its index in the table.
    Dim iWeightColumnIndex As Long
    iWeightColumnIndex = GetColumnIndex(oFactory, "Weight")
    If iWeightColumnIndex = -1 Then
        MsgBox "The column ""weight"" does not exist in the table."
        Exit Sub
    End If

    ' Iterate through the rows
    Dim oRow As iPartTableRow
    For Each oRow In oFactory.TableRows
        ' Make this the active row so the model will recompute.
        oFactory.DefaultRow = oRow

        ' Get the weight.
        Dim dWeight As Double
        dWeight = oPartDoc.ComponentDefinition.MassProperties.Mass

        ' Convert it to current mass units defined by the document.
        Dim strWeight As String
        strWeight = oPartDoc.UnitsOfMeasure.GetStringFromValue( _ 
                                dWeight, kDefaultDisplayMassUnits)

        ' Set the row value for the weight column.
        oRow.Item(iWeightColumnIndex).Value = strWeight
    Next
End Sub


’ Function that given a factory and the name or a column will return
’ the index number of the column, if it’s found in the factory’s
’ table.  If the column is not found it returns –1.  The comparison
’ of the name is done in a case insensitive way.
Private Function GetColumnIndex(ByVal Factory As iPartFactory, _ 
                                ByVal ColumnName As String) As Long
    ‘ Iterate through all of the columns looking for a
    ‘ match to the input name.
    Dim i As Long
    For i = 1 To Factory.TableColumns.Count
        Dim oColumn As iPartTableColumn
        Set oColumn = Factory.TableColumns.Item(i)

        ‘ Compare this column with the input name.
        If LCase(oColumn.DisplayHeading) = LCase(ColumnName) Then
            ' A matching column was found so exit.
            GetColumnIndex = i
            Exit Function
        End If
    Next

    ' The column wasn't found so return -1.
    GetColumnIndex = -1
End Function


The program is essentially performing the same steps that would be required to do this manually.  It’s just able to do it much faster and without any mistakes.  After running the macro on my factory the table now looks like this.

iPartTableAfter


It’s also important to recognize that the weight is a snapshot of the weight at the time the macro was run.  If the part is modified (for example, a new feature is added) or table values are edited that affect the part geometry, the weight will no longer be correct and the macro will need to be run again to update the weight column.

RSS Feed

Search