Modifying "Subroutine to Process All Components in Assembly" to work with features

Hi,

As background, I decided to register on this forum after getting excellent help from user cowski over on the Eng-Tips forum. Over there I had originally asked how to get the coordinates (position and orientation angles) of all selected parts. Also, I have zero experience with VB/.NET, and can barely make my way around NX yet, so patience is appreciated.

After playing around with the examples I was given, and spending quite a while messing around with several examples I've found on this forum, I've returned with a different question.

Before I ask that question, however, I want to state my ultimate goal, so we don't go down an x-y problem rabbit hole.

My goal is to:

  • Make a button (DONE)
  • That when pressed, does the following:
    • Walks through the work part, which is an assembly and has components and subassemblies (DONE, there's a tutorial on that),
    • looking for DatumCSYS features with a specific, identical name (NOT DONE, subject of this post)
    • and exporting their position and orientation coordinates to the ListingWindow (DONE, I can adapt the example code cowski gave me)

    My question then is: how do I query properties of a Feature like a DatumCSYS from a Component?

    I know how to find the DatumCSYS I am looking for with this snippet, which iterates over the features of the work part:

    Const target_DatumCSYS As String = "special_coordinate_system_name"
    For Each myFeature As Feature In workPart.Features
    If TypeOf (myFeature) Is DatumCsys Then
    If (myFeature.Name = target_DatumCSYS) Then
    '' Coordinate-extracting code would go here, etc.
    lw.WriteLine(myFeature.OwningComponent.Name)
    End If
    End If
    Next

    I want to use this snippet inside the example given in "Creating A Subroutine To Process All Components In An Assembly". In that example, members of the top-level assembly are iterated over, and entered into recursively, via

    For Each child As Component In comp.GetChildren()

    So I figured maybe I could just use child.Features, but that doesn't work.

    I looked up the class hierarchy for a Component and a Feature in the reference, and it is:

    TaggedObject NXObject Feature DatumCSYS
    TaggedObject NXObject DisplayableObject Component

    And this is where I'm completely stuck. Should the main iteration be over NXObject instead of Component, casting almost everything to Component for the assembly tree traversal, and casting to Feature for my coordinate stuff?

    Any suggestions?

I've been thinking about more about this. I think I'm going to have to make each part the work part if I want to be able to access Features.

And from what I've read, all parts in the assembly need to be loaded to accomplish this, which makes sense. I've also come across example code to load unloaded parts, but to keep things simple, it's easier to load everything manually first.

Not sure if the fact I'm using Teamcenter changes anything.

I may try to give only the relevant components a special name in the top-level assembly, so I can

The feature information for component parts is only available if the component part is fully loaded. If the component is partially loaded or unloaded, the feature info will not be available. There are ways to fully load a component part file through code, but to focus on the task at hand, we'll assume that your components are fully loaded.

The component object does not give you access to the feature collection; for this you will need the part object. Fortunately, getting the part from the component is fairly easy; the prototype of the component is the part. The code that I use to get the part from the component looks something like this:

dim myPart as Part
myPart = myComponent.Prototype.OwningPart

Now that you have a part object, you can access its features.

For each someFeature as Features.Feature in myPart.Features
'do something with someFeature
Next

Edit: I must have been typing my response when you posted, so I didn't see your update post until just now. You will NOT need to make the part of interest the work part to access the features (but it will need to be fully loaded). If the part is only partially loaded, making it the work part is one way (but not the only way) to fully load it. Teamcenter doesn't really make a difference in this case.

Thank you for your reply! That helps. I have decided to require that the entire assembly is fully loaded before running the journal for simplicity.

To explore your suggestion of using I modified "Creating A Subroutine To Process All Components In An Assembly"


dim myPart as Part
myPart = myComponent.Prototype.OwningPart

For each someFeature as Features.Feature in myPart.Features
'do something with someFeature
Next

I tried adding very simple code to


Sub reportComponentChildren( ByVal comp As Component, ByVal indent As Integer)

to confirm that I have access to features. (Remember, my ultimate goal is to get coordinates from a DatumCSYS feature).

The simple test code I added was:


myPart = child.Prototype.OwningPart
For each someFeature as Features.Feature in myPart.Features
'do something with someFeature
lw.WriteLine("feature name: " & someFeature.GetFeatureName)
lw.WriteLine("feature type: " & someFeature.GetType.ToString)
lw.WriteLine("")
Next

When I placed this code block in the part of reportComponentChildren() that is "to process component or subassembly", the top level Try block caught an exception related to ""Object reference not set to an instance of an object"" which I'm pretty sure was related to this line:


For each someFeature as Features.Feature in myPart.Features

One thing I haven't mentioned yet is that I am only interested in the features of components whose name match a special pattern that I filter for. I know in advance that these components are leaf nodes of the assembly tree. Therefore, I moved the Feature test code under the part of reportComponentChildren() where

if child.GetChildren.Length <> 0 then

is false, i.e. there are no children under "child". The problem is that there is no output generated in the ListingWindow, which means that


For each someFeature as Features.Feature in myPart.Features

never executed. Any ideas?

Also, thank you for clearing up that a component doesn't have to be the work part in order to access its Features. I also read up on Chapter 11 of the SNAP introduction you suggested in another post, and that cleared up things regarding prototypes and components for me. That said, I'm a bit stuck because pretty much every example I can find on Features seems to start with the work part, and I have a feeling the problem here is that I'm iterating through an assembly.

Here's the full code:


'Journal to recursively walk through the assembly structure
' will run on assemblies or piece parts
' will step through all components of the displayed part
'NX 7.5, native
'NXJournaling.com February 24, 2012
' MODIFIED

Option Strict Off

Imports System
Imports NXOpen
Imports NXOpen.UF
Imports NXOpen.Assemblies
Imports System.Text.RegularExpressions

Module NXJournal

Public theSession As Session = Session.GetSession()
Public ufs As UFSession = UFSession.GetUFSession()
Public lw As ListingWindow = theSession.ListingWindow

Sub Main()
Dim workPart As Part = theSession.Parts.Work
Dim dispPart As Part = theSession.Parts.Display

lw.Open
Try
Dim c As ComponentAssembly = workPart.ComponentAssembly
if not IsNothing(c.RootComponent) then
'*** insert code to process 'root component' (assembly file)
lw.WriteLine("Assembly: " & c.RootComponent.DisplayName & ", Active Arrangement: " & c.ActiveArrangement.Name)
'*** end of code to process root component
ReportComponentChildren(c.RootComponent, 0)
else
'*** insert code to process piece part
lw.WriteLine("Work Part has no components. Please make the top-level assembly the work part.")
end if
Catch e As Exception
theSession.ListingWindow.WriteLine("Failed: " & e.ToString)
End Try
lw.Close

End Sub

'**********************************************************
Sub reportComponentChildren( ByVal comp As Component, _
ByVal indent As Integer)

'Will be used to access Features for each specially named component
dim myPart as Part

'Set up variables we will use to look for specially named components
Dim component_name_tag As String = ""
Dim strRemove As String = "PATTERN-"
Dim regex As New Regex(strRemove, RegexOptions.IgnoreCase)

For Each child As Component In comp.GetChildren()
'*** insert code to process component or subassembly

'Look for components with a name that starts with "PATTERN-"
'and write the first two columns of output: part number and special component name.
component_name_tag = child.Name
If (child.Name.StartsWith(strRemove)) Then
component_name_tag = regex.Replace(component_name_tag , "")
lw.WriteLine(child.DisplayName() & ControlChars.Tab & component_name_tag )

'*** end of code to process component or subassembly

if child.GetChildren.Length <> 0 then
'*** this is a subassembly, add code specific to subassemblies
lw.WriteLine("This has children")

'*** end of code to process subassembly
else
'this component has no children (it is a leaf node)
'add any code specific to bottom level components
lw.WriteLine("This has no children")

myPart = child.Prototype.OwningPart
For each someFeature as Features.Feature in myPart.Features
'do something with someFeature
lw.WriteLine("feature name: " & someFeature.GetFeatureName)
lw.WriteLine("feature type: " & someFeature.GetType.ToString)
lw.WriteLine("")
Next

end if ' End if child.GetChildren.Length <> 0

End If ' End If this is a specially named component

reportComponentChildren(child, indent + 1)
Next
End Sub
'**********************************************************
Public Function GetUnloadOption(ByVal dummy As String) As Integer
Return Session.LibraryUnloadOption.Immediately
End Function
'**********************************************************

End Module

Thank you!

I think that you are getting "comp" and "child" confused in the code. In the "reportComponentChildren" sub, we are examining "comp" (which is a component object). If the component has children (comp.GetChildren), then it is a subassembly and we process each child object later. If it does not have any children, it (comp) is a leaf node.

Instead of:

myPart = child.Prototype.OwningPart

Try:

myPart = comp.Prototype.OwningPart

That's a great point, thank you. Since this function is recursive, we will get to everything eventually *without* having to process it as a child.

I'm sorry there's a few incomplete sentences above, etc., but there doesn't seem to be a way to edit my reply.

Ok, this is weird. I made an assembly one level below my top-level assembly the work part, and then made my top-level assembly the work part again. I ran the script again, with no changes, and now I'm getting features for some of my specially-named components.

This is the ListingWindow output:

Assembly: XXXXXXXX/001, Active Arrangement: Arrangement 1
YYYYYYYY/001 DUMMY2
This has no children
feature name: Datum Coordinate System(0)
feature type: NXOpen.Features.DatumCsys

feature name: SKT_000:Sketch(0)
feature type: NXOpen.Features.SketchFeature

feature name: Extrude(1)
feature type: NXOpen.Features.Extrude

(...)

WWWWWWWW/001.0005 DUMMY3
This has no children
ZZZZZZZZ/001.0002 DUMMY4
This has no children

DUMMY3 and DUMM4 definitely have features. When I make them the work part I can see lots of features in the Part Navigator under "Model History". Same for DUMMY1 and DUMMY2.

What's going on here?

More information: I made one of the components that showed no features the work part, and then again made my top-level feature the work part. Now I am getting feature information from it. It looks like whatever I am doing manually in NX is not fully loading all child components in a way that this script needs. I'll look into how to do this, tips welcome!

One way to fully load the components is to change the assembly load options before opening the assembly. In the "scope" section of the assembly load options, turn off the "use partial loading" option.

Don't use the "load structure only" option as this won't load any component data.

After the assembly is loaded, you can right click on a component in the navigator; if there is an option like "load fully", then the component is not fully loaded yet.

Thank you, I will look into this. I had shied away from changing Assembly Load Options because I didn't know where this setting lives: the user's local NX installation, or the assembly itself. Since I want this script to have as few prerequisite configuration actions as possible, I had avoided this. I will look into changing the setting and where it is saved/who it applies to.

We can add code later that will ensure each component is fully loaded; however, to get up and running, it will be easier to fully load the assembly before running your test code.

In most of the installations that I've seen, the user can change their load options without affecting anyone else. However, it is possible that a shared load options file is used. The load options are not saved in the assembly file itself.

Ok, I've made good progress. I still don't know why I don't seem to be loading the full assembly properly (not sure if I should be using Open --> Components or Open --> Assembly, maybe that's the issue). However, I am now able to filter for the DatumCSYS features in my special components, and get some coordinates.

As a reminder, my ultimate goal is to get coordinates and orientation of this DatumCSYS, but in the absolute coordinate system.

Currently this block of code:


Dim DBuilder As DatumCsysBuilder
DBuilder = myPart.Features.CreateDatumCsysBuilder(someFeature) lw.WriteLine(someFeature.Location.ToString) lw.WriteLine(DBuilder.Csys.Orientation.Element.ToString)
lw.WriteLine("")
DBuilder.Destroy()

prints coordinates in what looks like the part's coordinate system:


XXXXXXX/001 DUMMY1
This has no children
[X=110,Y=-25,Z=12.5]
[Xx=1,Xy=0,Xz=0,Yx=0,Yy=1,Yz=0,Zx=0,Zy=0,Zz=1]

When I select the DatumCSYS in NX and get Information --> Object, the coordinates are


Origin location in Absolute Coordinate System and WCS
Origin Location 1760.0 -425.0 2412.5
1760.0 -425.0 2412.5
X-axis Vector (Y-Z Plane) in Absolute Coordinate System and WCS
X-axis Vector
1.0 0.0 0.0
1.0 0.0 0.0
Y-axis Vector (X-Z Plane) in Absolute Coordinate System and WCS
Y-axis Vector
0.0 1.0 0.0
0.0 1.0 0.0
Z - axis Vector(X - Y Plane) in Absolute Coordinate System and WCS
Z-axis Vector
0.0 0.0 1.0
0.0 0.0 1.0

I've searched around quite a bit but can't seem to find how to get these as absolute coordinates. Probably because my understanding of NX coordinate systems is still shaky.

The solution probably is something like creating a new Point3D at 0,0,0, which uses absolute coordinates, then maybe placing a temporary, new DatumCSYS at this POint3D location, then... reporting my DatumCSYS coordinates from the perspective of the 0,0,0 DatumCSYS.

Or maybe, given the result of Information --> Object, my WCS is already sitting at 0,0,0...

First of all, I'm happy to start a new thread, just let me know.

Ok, I've tried three different ways to get the absolute coordinates of this special DatumCSYS feature that is a part of my special component, with no luck. I keep getting the coordinates in what I suspect is the component's coordinate system.

If I make the special component the work part in NX, and double click on the special DatumCSYS, I see it is of type Dynamic, and the reference is Absolute - Displayed Part. A small popup shows up with its absolute coordinates, which are in the thousands of mm, which is what I expect. These are the coordinates as I understand them in the Absolute coordinate system, right?

However these three methods of querying the coordinates give me XYZ positions in the tens of mm, which suggests that these coordinates of my DatumCSYS are being given in some local coordinate system of the component.

One of the methods I used uses


Function WCS2Abs(ByVal inPt As Point3d) As Point3d
Dim pt1(2), pt2(2) As Double

pt1(0) = inPt.X
pt1(1) = inPt.Y
pt1(2) = inPt.Z

ufs.Csys.MapPoint(UFConstants.UF_CSYS_ROOT_WCS_COORDS, pt1, _
UFConstants.UF_CSYS_ROOT_COORDS, pt2)

WCS2Abs.X = pt2(0)
WCS2Abs.Y = pt2(1)
WCS2Abs.Z = pt2(2)

End Function

to convert these coordinates, but no matter what combination of UFConstants I used (UF_CSYS_ROOT_WCS_COORD, UF_CSYS_ROOT_COORDS, UF_CSYS_WORK_COORDS ), the result never changed: I get the same coordinates.

Moreover: I have two instances of this same special component, which I have named DUMMY1 and DUMMY2. Both report identical coordinates via the 3 methods I used, which again suggests that I am not able to get the special DatumCSYS coordinates in Absolute.

This is the output to ListingWindow:


Assembly: XXXXXXX/001, Active Arrangement: Arrangement 1
YYYYYYY/001 DUMMY2
######### 1st approach
Location: [X=110,Y=-25,Z=12.5]
Orientation: [Xx=1,Xy=0,Xz=0,Yx=0,Yy=1,Yz=0,Zx=0,Zy=0,Zz=1]

######### 2nd approach
from method 2: 110 -25 12.5

######### 3rd approach
Origin: 110 -25 12.5
Element x: 1 0 0
Element y: 0 1 0
Element z: 0 0 1
Converted: 110 -25 12.5

############################################
YYYYYYY/001 DUMMY1
######### 1st approach
Location: [X=110,Y=-25,Z=12.5]
Orientation: [Xx=1,Xy=0,Xz=0,Yx=0,Yy=1,Yz=0,Zx=0,Zy=0,Zz=1]

######### 2nd approach
from method 2: 110 -25 12.5

######### 3rd approach
Origin: 110 -25 12.5
Element x: 1 0 0
Element y: 0 1 0
Element z: 0 0 1
Converted: 110 -25 12.5

And this is the code:


'Journal to recursively walk through the assembly structure
' will run on assemblies or piece parts
' will step through all components of the displayed part
'NX 7.5, native
'NXJournaling.com February 24, 2012

Option Strict Off

Imports System
Imports NXOpen
Imports NXOpen.UF
Imports NXOpen.Assemblies
Imports NXOpen.Features
Imports System.Text.RegularExpressions

Module NXJournal

Public theSession As Session = Session.GetSession()
Public ufs As UFSession = UFSession.GetUFSession()
Public lw As ListingWindow = theSession.ListingWindow

Sub Main()

lw.Open

If IsNothing(theSession.Parts.Work) Then
lw.WriteLine("Active part required.")
Return
End If

Dim workPart As Part = theSession.Parts.Work
Dim dispPart As Part = theSession.Parts.Display

Try
Dim c As ComponentAssembly = workPart.ComponentAssembly
If not IsNothing(c.RootComponent) then
'*** insert code to process 'root component' (assembly file)
lw.WriteLine("Assembly: " & c.RootComponent.DisplayName & ", Active Arrangement: " & c.ActiveArrangement.Name)
'*** end of code to process root component
ReportComponentChildren(c.RootComponent, 0)
else
'*** insert code to process piece part
lw.WriteLine("Work Part has no components. Please make the top-level assembly the work part.")
end if
Catch e As Exception
theSession.ListingWindow.WriteLine("Failed: " & e.ToString)
End Try
lw.Close

End Sub

'**********************************************************
Sub reportComponentChildren( ByVal comp As Component, _
ByVal indent As Integer)

'Will be used to access Features for each component
dim myPart as Part

'Set up variables we will use to look for special components
Dim special_name_tag As String = ""
Dim strRemove As String = "SPECIAL-"
Dim regex As New Regex(strRemove, RegexOptions.IgnoreCase)

For Each child As Component In comp.GetChildren()
'*** insert code to process component or subassembly

'Look for components with a name that starts with "SPECIAL-"
'and write the first two columns of output: part number and special component number.
special_name_tag = child.Name
If (child.Name.StartsWith(strRemove)) Then
special_name_tag = regex.Replace(special_name_tag, "")
lw.WriteLine(child.DisplayName() & ControlChars.Tab & special_name_tag)

'*** end of code to process component or subassembly

if child.GetChildren.Length <> 0 then
'*** this is a subassembly, add code specific to subassemblies
lw.WriteLine("This has children")

'*** end of code to process subassembly
else
'this component has no children (it is a leaf node)
'add any code specific to bottom level components
'lw.WriteLine("This has no children")

myPart = child.Prototype.OwningPart
For each someFeature as Features.Feature in myPart.Features
'do something with someFeature
If (TypeOf (someFeature) Is DatumCsys) AndAlso (Not someFeature.IsInternal) Then
If (someFeature.Name = "special_coordinate_system") Then
'lw.WriteLine("feature name: " & someFeature.GetFeatureName)
'lw.WriteLine("custom name: " & someFeature.Name)
'lw.WriteLine("feature type: " & someFeature.GetType.ToString)
'lw.WriteLine("#####")

'Code to get coordinates goes here

''''1) 1st approach
lw.WriteLine("######### 1st approach")
Dim DBuilder As DatumCsysBuilder
DBuilder = myPart.Features.CreateDatumCsysBuilder(someFeature)
lw.WriteLine("Location: " & someFeature.Location.ToString)
lw.WriteLine("Orientation: " & DBuilder.Csys.Orientation.Element.ToString)
lw.WriteLine("")
DBuilder.Destroy()

''''2) 2nd approach
lw.WriteLine("######### 2nd approach")
Dim theDatumCsys As NXOpen.Features.DatumCsys = CType(someFeature, NXOpen.Features.DatumCsys)
Dim theEntities() As NXObject = theDatumCsys.GetEntities()

Dim csys_tag As Tag
Dim origin_tag As Tag
Dim daxes As Tag()
Dim dplanes As Tag()
ufs.Modl.AskDatumCsysComponents(theDatumCsys.Tag, csys_tag, origin_tag, daxes, dplanes)
Dim datumOrigin As Point = Utilities.NXObjectManager.Get(origin_tag)
lw.WriteLine("from method 2: " & datumOrigin.Coordinates.X.toString & " " & datumOrigin.Coordinates.Y.toString & " " & datumOrigin.Coordinates.Z.toString)
lw.WriteLine("")

''''3) Third approach.
lw.WriteLine("######### 3rd approach")
For Each ent As NXObject In theEntities

If TypeOf ent Is CartesianCoordinateSystem Then
Dim theCsys As NXOpen.CartesianCoordinateSystem = CType(ent, CartesianCoordinateSystem)
'Dim coordinates() As Double = New Double(2) {theCsys.Origin.X, theCsys.Origin.Y, theCsys.Origin.Z}
lw.WriteLine("Origin:" & ControlChars.Tab _
& theCsys.Origin.X.toString() & ControlChars.Tab _
& theCsys.Origin.Y.toString() & ControlChars.Tab _
& theCsys.Origin.Z.toString())
lw.WriteLine("Element x:" & ControlChars.Tab _
& theCsys.Orientation.Element.Xx.toString() & ControlChars.Tab _
& theCsys.Orientation.Element.Yx.toString() & ControlChars.Tab _
& theCsys.Orientation.Element.Zx.toString())
lw.WriteLine("Element y:" & ControlChars.Tab _
& theCsys.Orientation.Element.Xy.toString() & ControlChars.Tab _
& theCsys.Orientation.Element.Yy.toString() & ControlChars.Tab _
& theCsys.Orientation.Element.Zy.toString())
lw.WriteLine("Element z:" & ControlChars.Tab _
& theCsys.Orientation.Element.Xz.toString() & ControlChars.Tab _
& theCsys.Orientation.Element.Yz.toString() & ControlChars.Tab _
& theCsys.Orientation.Element.Zz.toString())

Dim pt11 As Point3d = New Point3d(theCsys.Origin.X, theCsys.Origin.Y, theCsys.Origin.Z)
Dim pt22 As Point3d = New Point3d(0, 0, 0)
pt22 = Wrk2WCS(pt11) ' Also tried WCS2Wrk
lw.WriteLine("Converted: " & pt22.X.toString & " " & pt22.Y.toString & " " & pt22.Z.toString)

lw.WriteLine("")
lw.WriteLine("############################################")
End If

Next

End If 'End If match for DatumCSYS name
End If 'End If match for type DatumCSYS and not internal
Next

end if ' End if child.GetChildren.Length <> 0

End If ' End If this is a special component

reportComponentChildren(child, indent + 1)
Next
End Sub
'**********************************************************
Public Function GetUnloadOption(ByVal dummy As String) As Integer
Return Session.LibraryUnloadOption.Immediately
End Function
'**********************************************************

Function Wrk2WCS(ByVal inPt As Point3d) As Point3d
Dim pt1(2), pt2(2) As Double

pt1(0) = inPt.X
pt1(1) = inPt.Y
pt1(2) = inPt.Z

ufs.Csys.MapPoint(UFConstants.UF_CSYS_WORK_COORDS, pt1, _
UFConstants.UF_CSYS_ROOT_WCS_COORDS, pt2)

Wrk2WCS.X = pt2(0)
Wrk2WCS.Y = pt2(1)
Wrk2WCS.Z = pt2(2)

End Function

Function WCS2Wrk(ByVal inPt As Point3d) As Point3d
Dim pt1(2), pt2(2) As Double

pt1(0) = inPt.X
pt1(1) = inPt.Y
pt1(2) = inPt.Z

ufs.Csys.MapPoint(UFConstants.UF_CSYS_ROOT_WCS_COORDS, pt1, _
UFConstants.UF_CSYS_ROOT_COORDS, pt2)

WCS2Wrk.X = pt2(0)
WCS2Wrk.Y = pt2(1)
WCS2Wrk.Z = pt2(2)

End Function

End Module

When you interrogate the feature, you are getting the coordinates with respect to the component part file, not the assembly. Given these coordinates and the component information, you could calculate (or map) the resulting coordinates in the assembly space. However, it might be easier to get the prototype coordinate system object from the datum csys feature, then find the resulting occurrence coordinate system in the assembly (using the handy .FindOccurrence method) and ask for its coordinates.

Thank you, that seems to be what's going on.

The first option would probably mean querying the position and orientation of the component in the top level assembly (which I can do, given an example you gave a few days ago on another forum), and then, assuming those coordinates are of the origin of the component, I can easily transform to the local coordinates of the DatumCSYS I'm getting here.

I haven't looked into FindOcurrence yet. I will look into it. Since all of these instances of the special DatumCSYS have the same name, I would have to sort them out by also determining what component they are a part of, as the different instances of the components are identified by custom names I entered at the level of the top assembly.

"I would have to sort them out by also determining what component they are a part of..."

In the "processAssembly" code that you started with, you already have a reference to the component that the csys comes from.

True!

I've decided to start with the approach that adds the assembly-level coordinates for the component (obtained with .GetPosition(pt, RotMat)) to the coordinates reported for the DatumCSYS inside the component (obtained with someFeature.Location) and it works—as long as the component isn't rotated, because I haven't accounted for rotations yet. I'll try to get this bit working next.

One thing I am concerned about is that my DatumCSYS is so far not rotated within the component. I don't foresee a situation where these special DatumCSYS will have to be rotated within the component, but I wonder if there will be an extra transformation to be done if that is the case.

Anyway, I'll try to get the simple, non-rotated case working first.

Ok, for better of for worse I have decided to take the approach where I transform the DatumCSYS coordinates in the part's reference frame into the assembly reference frame. For this initial example I am assuming the DatumCSYS is not rotated within the component.

In this case, I take the vector representing the coordinates of the DatumCSYS in the component's frame, and apply the rotation matrix that I got when I queried the component's orientation ( with GetPosition()). This gives me the offsets I must apply to the coordinates of the part in the assembly frame. (My first attempt did not work, so I had to transpose the matrix).

It's works... for the first instance of the component (DUMMY1). It does not for the second, and the signs are all wrong for the third. All three instances of the component are in different orientations. I'll probably figure it out through trial and error... eventually. I'll check back in with my progress.

It works!

I cannot thank you enough for your help! Assuming user NXJournaling is cowski, I was amazed to see that you seem to be in every single google result I came across while trying to get this to work over a few weeks, patiently dropping knowledge and helping people, for years! You're truly a gem of the NXOpen community.

Here's an example of the output for 11 components, all placed at 1000,1000,1000 absolute, and rotated in roll, pitch, and yaw (around x, y, and z axes in order) by different amounts. They all contain a DatumCSYS feature that is at 100,100,100 in the component's local coordinate system, and is not rotated.


Assembly: XXXXXXXX/001, Active Arrangement: Arrangement 1
YYYYYYYY/001 DUMMY12_R145_P52_Y-133 869.352049841041 1064.11010446167 906.079780530621 145 51.9999999999999 -133
YYYYYYYY/001 DUMMY11_R-35_P128_Y-313 869.352049841041 1064.11010446167 906.079780530621 145 52.0000000000001 -133
YYYYYYYY/001 DUMMY10_R0_P10_Y30 1050.32522651329 1144.5253369124 1081.11595753453 0 9.99999999999999 30
YYYYYYYY/001 DUMMY9_R30_P0_Y10 1092.12481086652 1053.41128331133 1136.60254037844 30 0 10.0000000000001
YYYYYYYY/001 DUMMY8_R30_P10_Y0 1122.20155750261 1036.60254037844 1117.16242307916 30 9.99999999999999 0
YYYYYYYY/001 DUMMY7_R0_P0_Y-30 1136.60254037844 1036.60254037844 1100 0 0 -30
YYYYYYYY/001 DUMMY6_R0_P0_Y30 1036.60254037844 1136.60254037844 1100 0 0 30
YYYYYYYY/001 DUMMY5_R0_P-30_Y0 1036.60254037844 1100 1136.60254037844 0 -30 0
YYYYYYYY/001 DUMMY4_R0_P30_Y0 1136.60254037844 1100 1036.60254037844 0 30 0
YYYYYYYY/001 DUMMY3_R-45_P0_Y0 1100 1141.42135623731 1000 -45 0 0
YYYYYYYY/001 DUMMY2_R0_P0_Y0 1100 1100 1100 0 0 0
YYYYYYYY/001 DUMMY1_R45_P0_Y0 1100 1000 1141.42135623731 44.9999999999999 0 0

And here's the code. I never made the change from 'child' to 'comp' you suggested above. Let sleeping dogs lie...


' Looks for components of the assembly that have been given a name
'(via Properties->General->Name) that starts with "SPECIAL_COMPONENT-".
'For those components it looks for a DatumCSYS feature called
' "SPECIAL_COMPONENT_coordinate_system" and reports the position
' in absolute coordinates, and the Roll, Pitch, Yaw angles with respect to the
' X, Y, Z axes, in order.
'Top-level assembly must be work part.

Option Strict Off

Imports System
Imports NXOpen
Imports NXOpen.UF
Imports NXOpen.Assemblies
Imports NXOpen.Features
Imports System.Text.RegularExpressions
Imports NXOpen.MathUtils

Module NXJournal

Public theSession As Session = Session.GetSession()
Public ufs As UFSession = UFSession.GetUFSession()
Public lw As ListingWindow = theSession.ListingWindow

Sub Main()

lw.Open

If IsNothing(theSession.Parts.Work) Then
lw.WriteLine("Active part required.")
Return
End If

Dim workPart As Part = theSession.Parts.Work
Dim dispPart As Part = theSession.Parts.Display

Try
Dim c As ComponentAssembly = workPart.ComponentAssembly
If not IsNothing(c.RootComponent) then
'*** insert code to process 'root component' (assembly file)
lw.WriteLine("Assembly: " & c.RootComponent.DisplayName & ", Active Arrangement: " & c.ActiveArrangement.Name)
'*** end of code to process root component
ReportComponentChildren(c.RootComponent, 0)
else
'*** insert code to process piece part
lw.WriteLine("Work Part has no components. Please make the top-level assembly the work part.")
end if
Catch e As Exception
theSession.ListingWindow.WriteLine("Failed: " & e.ToString)
End Try
lw.Close

End Sub

'**********************************************************
Sub reportComponentChildren( ByVal comp As Component, _
ByVal indent As Integer)

'Will be used to access Features for each component SPECIAL_COMPONENT
dim myPart as Part

'Set up variables we will use to look for tagged SPECIAL_COMPONENTs
Dim SPECIAL_COMPONENT_name_tag As String = ""
Dim strRemove As String = "SPECIAL_COMPONENT-"
Dim regex As New Regex(strRemove, RegexOptions.IgnoreCase)

For Each child As Component In comp.GetChildren()
'*** insert code to process component or subassembly

'Look for components with a name that starts with "SPECIAL_COMPONENT-"
'and write the first two columns of output: part number and SPECIAL_COMPONENT tag.
SPECIAL_COMPONENT_name_tag = child.Name
If (child.Name.StartsWith(strRemove)) Then
SPECIAL_COMPONENT_name_tag = regex.Replace(SPECIAL_COMPONENT_name_tag, "")

'*** end of code to process component or subassembly

if child.GetChildren.Length <> 0 then
'*** this is a subassembly, add code specific to subassemblies
'*** end of code to process subassembly
else
'this component has no children (it is a leaf node)
'add any code specific to bottom level components

myPart = child.Prototype.OwningPart
For each someFeature as Features.Feature in myPart.Features
'do something with someFeature
If (TypeOf (someFeature) Is DatumCsys) AndAlso (Not someFeature.IsInternal) Then
If (someFeature.Name = "SPECIAL_COMPONENT_coordinate_system") Then

'Code to get coordinates goes here
dim part_x As Double = someFeature.Location.X
dim part_y As Double = someFeature.Location.Y
dim part_z As Double = someFeature.Location.Z
dim part_pt As Point3D = someFeature.Location

Dim assy_coord_pt As Point3d
Dim RotMat_assy As Matrix3x3
child.GetPosition(assy_coord_pt, RotMat_assy)
Dim assy_x As Double = assy_coord_pt.x
Dim assy_y As Double = assy_coord_pt.y
Dim assy_z As Double = assy_coord_pt.z

dim part_x_in_assy As Double
dim part_y_in_assy As Double
dim part_z_in_assy As Double
dim part_in_assy_pt As Point3D

part_x_in_assy = RotMat_assy.xx*part_x + RotMat_assy.yx*part_y + RotMat_assy.zx*part_z
part_y_in_assy = RotMat_assy.xy*part_x + RotMat_assy.yy*part_y + RotMat_assy.zy*part_z
part_z_in_assy = RotMat_assy.xz*part_x + RotMat_assy.yz*part_y + RotMat_assy.zz*part_z

dim deltax as Double = assy_x + part_x_in_assy
dim deltay as Double = assy_y + part_y_in_assy
dim deltaz as Double = assy_z + part_z_in_assy

Dim angleX As Double 'rotation about X axis
Dim angleY As Double 'rotation about Y axis
Dim angleZ As Double 'rotation about Z axis
CompRot(child, angleX, angleY, angleZ)

Dim name_and_tag_string As String = child.DisplayName() _
& ControlChars.Tab & SPECIAL_COMPONENT_name_tag

lw.WriteLine(name_and_tag_string & ControlChars.Tab _
& deltax & ControlChars.Tab _
& deltay & ControlChars.Tab _
& deltaz & ControlChars.Tab _
& angleX & ControlChars.Tab _
& angleY & ControlChars.Tab _
& angleZ)

End If 'End If match for DatumCSYS name
End If 'End If match for type DatumCSYS and not internal
Next

end if ' End if child.GetChildren.Length <> 0

End If ' End If this is a tagged SPECIAL_COMPONENT

reportComponentChildren(child, indent + 1)
Next
End Sub
'**********************************************************
Public Function GetUnloadOption(ByVal dummy As String) As Integer
Return Session.LibraryUnloadOption.Immediately
End Function
'**********************************************************

Sub CompRot(ByVal someComponent As Component, ByRef RX1 As Double, ByRef RY1 As Double, ByRef RZ1 As Double)
'extract euler angles from rotation matrix:
'https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles.pdf

Dim pt3 As Point3d
Dim RotMat3 As Matrix3x3
someComponent.GetPosition(pt3, RotMat3)

Dim c1, c2, s1 As Double

RX1 = Math.Atan2(RotMat3.Yz, RotMat3.Zz)
c2 = Math.Sqrt(RotMat3.Xx ^ 2 + RotMat3.Xy ^ 2)
RY1 = Math.Atan2(-RotMat3.Xz, c2)
s1 = Math.Sin(RX1)
c1 = Math.Cos(RX1)
RZ1 = Math.Atan2(s1 * RotMat3.Zx - c1 * RotMat3.Yx, c1 * RotMat3.Yy - s1 * RotMat3.Zy)

'convert angles from radians to degrees
RX1 *= (180 / Math.PI)
RY1 *= (180 / Math.PI)
RZ1 *= (180 / Math.PI)

End Sub

Function CompPos(ByVal someComponent As Component) As Point3d
Dim pt As Point3d
Dim RotMat As Matrix3x3
someComponent.GetPosition(pt, RotMat)
Return pt
End Function

Sub PrintExpectedCoords(ByVal Name As String, ByVal xx As String, ByVal yy As String, ByVal zz As String)
lw.WriteLine(Name _
& ControlChars.Tab & ControlChars.Tab _
& xx & ControlChars.Tab _
& yy & ControlChars.Tab _
& zz)
End Sub

End Module

Glad to see you got it working. Thanks for posting your final result!