Wrangling the WCS

Having a working coordinate system (WCS) that can easily be moved around makes life easier for the designer. Choosing a convenient coordinate system and entering values directly is much simpler than transforming all the desired coordinates into the absolute coordinate system (ACS). The benefits of having a WCS are not only for the designers using interactive NX, programmers can also use the WCS to make their lives easier too. Moving the WCS around is fairly easy, much more so if you have a reference to a CartesianCoordinateSystem object or a Point3D and an orientation matrix; fortunately, many NX objects expose these object types as properties for our use.



Who's in charge here?

Rule #1 for working with the WCS: the display part, not the work part, controls the WCS - always. We need to define what NX means by 'display part' and 'work part'. The distinction only arises in the context of an assembly; if you have a piece part open, then the display part and work part are one and the same - the current part you have open. When you work with an assembly file, you can make a component the work part (right click on a component and choose 'make work part'). Any new geometry you create will belong to the work part. This is helpful for creating or modifying a component in the context of the assembly; you can easily see how the changes you make affect other parts in the assembly. In this case, the assembly is the display part and the component is the work part. If NX's title bar reads [part2.prt in Assembly part1.prt], you know that part2 is the work part and part1 is the display part.




So, what does this mean for us? The WCS is accessible through a part object; any calls we make to modify the WCS must go through the display part. If the display part and the work part are the same, you can use either to change the WCS; however, using the display part reference will always work. Using only the display part reference will make our programming task a bit easier as we won't have to keep track of work part vs. display part.



Items of interest

The WCS object has several properties and methods available for use, the ones that we'll focus on here are:

  • .CoordinateSystem
  • .Origin
  • .Rotate
  • .SetCoordinateSystem
  • .SetCoordinateSystemCartesianAtCsys
  • .SetOriginAndMatrix

.CoordinateSystem

The .CoordinateSystem property is read only, it will return a CartesianCoordinateSystem object that represents the WCS' orientation in space (the X, Y, and Z vectors that define the direction of each axis). The CartesianCoordinateSystem object will return the X and Y vectors (as Vector3d objects) through its .GetDirections method. If you want to see all the values in the orientation matrix, you will have to dig a bit deeper to find the elements; the code would look something like this:





'write the orientation matrix to the listing window
lw.WriteLine(displayPart.WCS.CoordinateSystem.Orientation.Element.ToString)

'access individual elements of the orientation matrix, the X axis elements in this case
lw.WriteLine(displayPart.WCS.CoordinateSystem.Orientation.Element.Xx.ToString)
lw.WriteLine(displayPart.WCS.CoordinateSystem.Orientation.Element.Xy.ToString)
lw.WriteLine(displayPart.WCS.CoordinateSystem.Orientation.Element.Xz.ToString)


.Origin

The .Origin property represents a Point3d object, the property is read/write; feel free to pass in a new Point3d to change the WCS' position.



.Rotate

The .Rotate method gives you an easy way to rotate the WCS around one of its axes. If we want to rotate the WCS by 30 degrees about its Z axis, the code would look like this:



displayPart.WCS.Rotate(WCS.Axis.ZAxis, 30)


Get your bearings

The last three methods that we'll look at: .SetCoordinateSystem, .SetCoordinateSystemCartesianAtCsys, and .SetOriginAndMatrix each allow you to specify the orientation and/or origin (you'll remember that the .CoordinateSystem property is read only). These methods were added at different times during the API's life and each works slightly differently, use the one that best meets your needs.



.SetCoordinateSystem

This method first showed up in NX 4; it takes a CartesianCoordinateSystem as a parameter and it returns a CartesianCoordinateSystem which represents the previous position of the WCS. In addition to returning an object for you (as a programmer) to use, it also creates a coordinate system object in the NX graphics window. If the user doesn't need or want this object, you can delete it at the end of your journal or you can use one of the other methods which do not create an object in the graphics window.



.SetCoordinateSystemCartesianAtCsys

This method first showed up in NX 7.5; it also takes a CartesianCoordinateSystem as a parameter and returns a CartesianCoordinateSystem object which represents the previous WCS position/orientation. The main difference from the previously discussed method is this one does NOT create a coordinate system object in the graphics window.



.SetOriginAndMatrix

This method debuted along with journaling itself in NX 3. This method takes two parameters: a Point3d structure and a Matrix3x3 structure. This method does not return any value, nor does it create any object in the graphics window. Many other NX objects, such as an Arc, expose an origin and orientation matrix as properties; this makes it easy to read these values from another object and assign them directly to the WCS.



Example Code

The journal code below was written and tested on NX 8.5. It should run on NX 7.5 and above, but I make no guarantees.



Option Strict Off
Imports System
Imports NXOpen

Module Module1

Sub Main()

Dim theSession As Session = Session.GetSession()
Dim workPart As Part = theSession.Parts.Work
Dim displayPart As Part = theSession.Parts.Display

Dim lw As ListingWindow = theSession.ListingWindow
lw.Open()

If IsNothing(displayPart) Then
'active part required
Return
End If

'save current WCS position and orientation
Dim oldOrigin As Point3d = displayPart.WCS.Origin
Dim oldOrientation As CartesianCoordinateSystem = displayPart.WCS.CoordinateSystem

'move WCS origin 10 units in +X (absolute) direction
Dim newOrigin As New Point3d(oldOrigin.X + 10, oldOrigin.Y, oldOrigin.Z)

'move WCS, the .SetOriginAndMatrix method does not save the old WCS
displayPart.WCS.SetOriginAndMatrix(newOrigin, oldOrientation.Orientation.Element)

MsgBox("Notice the WCS has moved in the +X (absolute) direction")

'rotate WCS
displayPart.WCS.Rotate(WCS.Axis.ZAxis, 32)
displayPart.WCS.Rotate(WCS.Axis.YAxis, 16)
displayPart.WCS.Rotate(WCS.Axis.XAxis, 8)

MsgBox("Notice the WCS has been rotated")

'move to absolute coordinate system
Dim absXform As Xform = displayPart.Xforms.CreateXform(SmartObject.UpdateOption.WithinModeling, 1)
Dim absCsys As CartesianCoordinateSystem = displayPart.CoordinateSystems.CreateCoordinateSystem(absXform, SmartObject.UpdateOption.WithinModeling)

'The .SetCoordinateSystem method returns a CartesianCoordinateSystem object and creates an NX coordinate system object in the graphics window.
'Assign it to a variable if you want to refer to it later (re-use it, delete it, etc).
Dim csys2 As CartesianCoordinateSystem = displayPart.WCS.SetCoordinateSystem(absCsys)

'lw.WriteLine(csys2.Orientation.Element.ToString)
MsgBox("The WCS is back at absolute and the previous position has been saved as an NX coordinate system object (the saved csys object might not be visible until after you press OK)")

Dim pt1 As Point3d = csys2.Origin
Dim startPt As Point = displayPart.Points.CreatePoint(pt1)
Dim vec1 As New Vector3d(csys2.Orientation.Element.Xx, csys2.Orientation.Element.Xy, csys2.Orientation.Element.Xz)

Dim dir2 As Direction = displayPart.Directions.CreateDirection(displayPart.Points.CreatePoint(pt1), vec1)
Dim dist2 As Scalar = displayPart.Scalars.CreateScalar(2, Scalar.DimensionalityType.Length, SmartObject.UpdateOption.WithinModeling)
Dim offset2 As Offset = displayPart.Offsets.CreateOffset(dir2, dist2, SmartObject.UpdateOption.WithinModeling)
Dim endPoint As Point = displayPart.Points.CreatePoint(offset2, startPt, SmartObject.UpdateOption.WithinModeling)

Dim rotX As Scalar = displayPart.Scalars.CreateScalar(30, Scalar.DimensionalityType.Angle, SmartObject.UpdateOption.WithinModeling)
Dim rotY As Scalar = displayPart.Scalars.CreateScalar(0, Scalar.DimensionalityType.Angle, SmartObject.UpdateOption.WithinModeling)
Dim rotZ As Scalar = displayPart.Scalars.CreateScalar(0, Scalar.DimensionalityType.Angle, SmartObject.UpdateOption.WithinModeling)

Dim myXform As Xform = displayPart.Xforms.CreateXform(csys2, startPt, endPoint, rotX, rotY, rotZ, 0, SmartObject.UpdateOption.WithinModeling, 1)
Dim newCsys As CartesianCoordinateSystem = displayPart.CoordinateSystems.CreateCoordinateSystem(myXform, SmartObject.UpdateOption.WithinModeling)

'The .SetCoordinateSystemCartesianAtCsys method returns a CartesianCoordinateSystem object, but does NOT create an NX coordinate system object in the graphics window.
Dim csys3 As CartesianCoordinateSystem = displayPart.WCS.SetCoordinateSystemCartesianAtCsys(newCsys)

'lw.WriteLine(csys3.Orientation.Element.ToString)

MsgBox("Finally, the WCS has been offset 2 units along the previously saved Csys' X direction and it has been rotated about the previous Csys' X axis")

End Sub

End Module




Smart Objects

Near the end of the code above, we used some 'smart objects' (Scalars, Offsets, and Xforms) to help us define the final coordinate system. Some more information on these objects can be found in the Point3d and Point objects: Part Two tutorial, if you are interested.



And now, for our special guest... The User!

Manipulating the WCS through code is well and good, but occasionally you will want the user to specify an orientation. The SpecifyCsys function is perfect for this. It will open a dialog box that allows the user to specify a coordinate system with one of NX's built in methods (dynamic, by object, origin + X point + Y point, etc etc). It is an older User Function (UF), so calling it is slightly different than the usual .NET functions/methods. The function takes 5 parameters as shown below:

Public Function SpecifyCsys ( _
title As String, _
ByRef option As Integer, _
csys_matrix As Double(), _
origin As Double(), _
ByRef csys_tag As Tag _
) As Integer


  • title {input only} - a prompt to the user
  • option {input/output} - an integer value that represents one of the methods of specifying a coordinate system (csys of object, csys of view, origin X point Y point, etc etc). The initial value passed into the function will determine the default value presented to the user, the value after the function completes will indicate the method the user actually chose to use.
  • csys_matrix {output} - a 9 element array of doubles that represents the orientation matrix of the coordinate system.
  • origin {output} - a three element array of doubles that represents the origin of the coordinate system.
  • csys_tag {input/output} - the tag of an existing coordinate system, this will be the default starting point presented to the user. A null value (Nothing in VB.net) can be passed into the function, in which case the current WCS will be used.

The return value of the function (an integer) represents the result of the dialog box.

  • 1 = user pressed Back
  • 2 = user pressed Cancel
  • 3 = user pressed OK
  • 7 = error: no active part
  • 8 = disallowed state, unable to bring up dialog




Example Code: Specify Csys

The code below was written and tested with NX 8.5, no guarantees for other versions.





Option Strict Off
Imports System
Imports System.Windows.Forms
Imports NXOpen
Imports NXOpen.UF

Module Module2

Public Enum csysOption As Integer
Inferred = 0
Origin_Xpt_Ypt = 1
Xaxis_Yaxis = 2
Zaxis_Xpoint = 3
Csys_object = 4
Dynamic = 5
OffsetCsys = 6
Absolute = 7
CurrentView = 8
ThreePlanes = 9
Xaxis_Yaxis_Origin = 10
Point_PerpendicularCurve = 11
Plane_Vector = 12
Plane_Xaxis_Pt = 13
Zaxis_Xaxis_Origin = 14
Zaxis_Yaxis_Origin = 15
End Enum

Sub Main()

Dim theSession As Session = Session.GetSession()
Dim theUfSession As UFSession = UFSession.GetUFSession
Dim theUISession As UI = UI.GetUI()

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

Dim lw As ListingWindow = theSession.ListingWindow
lw.Open()

If IsNothing(displayPart) Then
'active part required
Return
End If

'9 element orientation matrix of specified csys
'arrays are zero based
Dim myCsys(8) As Double

'3 element array: origin point of specified csys
Dim myOrigin(2) As Double

'tag variable used as input/output for the .SpecifyCsys method
'passing a null tag into the function uses the current WCS as the starting csys
Dim newCsysTag As Tag = Tag.Null

'variable to hold the user specified coordinate system
Dim newCsys As CartesianCoordinateSystem

Dim response As Integer

'If you want to know the option the user used to specify the csys, pass in a variable.
'The initial value of the variable will control the default option shown to the user.
'After the function returns, this variable will hold the actual option used.
Dim specifyOption As csysOption = csysOption.Dynamic

'optional message to user
MessageBox.Show("Orient manipulator to the desired location/orientation", "Specify Coordinate System", MessageBoxButtons.OK, MessageBoxIcon.Information)
theUISession.LockAccess()

response = theUfSession.Ui.SpecifyCsys("Specify Desired Orientation", specifyOption, myCsys, myOrigin, newCsysTag)

'if you don't care what option was used to specify the csys, simply pass in an integer that specified the default method to show to the user
'response = theUfSession.Ui.SpecifyCsys("Specify Desired Orientation", csysOption.Dynamic, myCsys, myOrigin, newCsysTag)

theUISession.UnlockAccess()

If response = Selection.Response.Ok Then
'get CartesianCoordinateSystem object from tag
newCsys = Utilities.NXObjectManager.Get(newCsysTag)
'orient WCS to specified coordinate system
displayPart.WCS.SetCoordinateSystemCartesianAtCsys(newCsys)

'try to parse enumeration value to integer (should always work in our case)
Dim num As Integer
If Not Integer.TryParse(specifyOption, num) Then
num = -1
End If

If num > -1 Then
MessageBox.Show("The user used option: " & specifyOption.ToString & " (option: " & num.ToString & ")", "Option used", MessageBoxButtons.OK, MessageBoxIcon.Information)
Else
MessageBox.Show("The user used option: " & specifyOption.ToString, "Option used", MessageBoxButtons.OK, MessageBoxIcon.Information)
End If

Else
'user pressed back or cancel or the system was in a state that could not allow the dialog to be shown
'1 = back
'2 = cancel
'3 = OK
'7 = no active part
'8 = disallowed state, unable to bring up dialog
End If

End Sub

End Module




Enumeration Elaboration

An enumeration is a way to group related constants and give them meaningful names. Since the dawn of programming, integer values have been used to represent program options. You might run across code like:

if optA = 2 then
.
.
end if



You would have to know what the '2' meant in context of the program logic. These types of values became known as 'magic constants', since the programmer working on the code needed some arcane, specialized knowledge of the program. Sure, while I'm programming the original code, I may know that option 4 = csys of object, but I'm not going to remember that next week, let alone 6 months from now when I pick up the code again to add a new feature. The value seems to work like magic, but I don't know why. Using an integer value to represent an option isn't a bad idea, but it does tend to obfuscate the resulting code, especially when the same integer value may represent something completely different in another part of the program. We create an enumeration at the module level to group the options and give them names that we will recognize later. Now that we have an enumeration, the IDE will help fill in the value I want. We now also have the advantage of using the .ToString method on the enumeration value; this will return the name we gave to the constant. The final message box in the journal may tell me "The user used option: Origin_Xpt_Ypt", this is much more useful to me than "The user used option: 1".




For more information on enumerations, see the following links:


Dot Net Pearls: VB.net Enum


MSDN Enum statement


MSDN How to: Declare enumerations

Comments

I was wondering weather there is a option to analyse all the existing CSYS inside a part and select only those which where created using the dynamic option.
I keep searching for a solution myself but it would be very nice if you could give a hint on how to do this.

Thanks a lot in advance.

In the example code above, the csys construction method is returned by the .SpecifyCsys function; the construction method is not saved with the resulting csys object. If you are examining existing csys objects, there is no way to tell how it was constructed. That information is only available at the time of creation from the .SpecifyCsys function.

Thanks for the reply. Although that clearly isn't the answer I was hoping for.
I thought there might be a way because if you reopen the dialog window of a DatumCSYS by hand it still is the specific dialog of the type like Offset CSYS or Dynamic CSYS therefore this information has to be safed somewhere but if there is no way of collecting that information by code I have to find another solution.
Thanks anyway.

Just to be clear: the article above is about specifying a generic coordinate system, this is separate from (but related to) datum csys features. If you have a datum csys feature, you should be able to get more information about its associativities from the object itself or from the datumCsysBuilder object.

I thought your question was in regards to the coordinate system objects, which do not show up in the feature tree; for these objects, there is no way of knowing how they were created.

I see. I thought that might be the difference. Thanks again for the clarification.
Although this therefore might be a bit OT but do you maybe have the exact command to get the type of a datum csys feature?
That would be fantastic cause I wasn't yet able to find one that worked myself.

This rabbit hole is deeper than I first anticipated. The code below looks for the "smart object" parents of the datum csys. I have not completed it yet, but based on the values returned it appears that you will be able to extrapolate the method used to construct the datum csys. Create or open a part file and create a variety of datum csys objects using different construction methods before running the journal. The code was written for NX 8.5, but I'm pretty sure it will run on other versions.

Option Strict Off
Imports System
Imports System.Collections.Generic
Imports NXOpen
Imports NXOpen.UF

Module datum_csys_parents

Dim theSession As Session = Session.GetSession()
Dim theUfSession As UFSession = UFSession.GetUFSession()

Dim lw As ListingWindow = theSession.ListingWindow

Sub Main()

If IsNothing(theSession.Parts.Work) Then
'active part required
Return
End If

Dim workPart As Part = theSession.Parts.Work
lw.Open()

Const undoMarkName As String = "NXJ journal"
Dim markId1 As Session.UndoMarkId
markId1 = theSession.SetUndoMark(Session.MarkVisibility.Visible, undoMarkName)

Dim datumCsysParents As New NXTree(Of TaggedObject)

For Each myFeature As Features.Feature In theSession.Parts.Work.Features.GetFeatures()
If myFeature.FeatureType = "DATUM_CSYS" Then

lw.WriteLine(myFeature.GetFeatureName & " '" & myFeature.Name & "'")
'lw.WriteLine(myFeature.FeatureType)

Dim csysTag As Tag
Dim originTag As Tag
Dim daxesTags(2) As Tag
Dim dplanesTags(2) As Tag
theUfSession.Modl.AskDatumCsysComponents(myFeature.Tag, csysTag, originTag, daxesTags, dplanesTags)

Dim theCsys As CoordinateSystem = Utilities.NXObjectManager.Get(csysTag)
Dim rootFeature As NXTreeNode(Of TaggedObject) = datumCsysParents.MakeRoot(theCsys)

GetSmartParents(rootFeature)

End If

Next

lw.WriteLine("")

lw.Close()

End Sub

Sub GetSmartParents(ByRef theSmartObject As NXTreeNode(Of TaggedObject))

Dim numParents As Integer
Dim theParentTags() As Tag = Nothing

Try
theUfSession.So.AskParents(theSmartObject.NodeObject.Tag, UFConstants.UF_SO_ASK_SO_PARENTS, numParents, theParentTags)

lw.WriteLine("number of parents: " & numParents.ToString)
For Each tempTag As Tag In theParentTags
Dim objParent As TaggedObject = Utilities.NXObjectManager.Get(tempTag)
lw.WriteLine("parent type: " & objParent.GetType.ToString)

GetSmartParents(theSmartObject.AddChild(objParent))

Next

lw.WriteLine("")

Catch ex As NXException
lw.WriteLine("error: " & ex.ErrorCode)
lw.WriteLine(" " & ex.Message)
End Try

End Sub

Public Function GetUnloadOption(ByVal dummy As String) As Integer

'Unloads the image when the NX session terminates
GetUnloadOption = NXOpen.Session.LibraryUnloadOption.AtTermination

'----Other unload options-------
'Unloads the image immediately after execution within NX
'GetUnloadOption = NXOpen.Session.LibraryUnloadOption.Immediately

'Unloads the image explicitly, via an unload dialog
'GetUnloadOption = NXOpen.Session.LibraryUnloadOption.Explicitly
'-------------------------------

End Function

End Module

#Region "NXTree and Node classes"

Public Class NXTree(Of T)

Private m_Root As NXTreeNode(Of T) = Nothing
' _
Public Property Root() As NXTreeNode(Of T)
Get
Return m_Root
End Get

Set(ByVal value As NXTreeNode(Of T))
m_Root = value
End Set

End Property

Public Sub Clear()
m_Root = Nothing
End Sub

Public Function MakeRoot(ByVal node_item As T) As NXTreeNode(Of T)
m_Root = New NXTreeNode(Of T)(node_item)
Return m_Root
End Function

Public Overrides Function ToString() As String
Return m_Root.ToString()
End Function

End Class

Public Class NXTreeNode(Of T)
Public NodeObject As T
Public Children As New List(Of NXTreeNode(Of T))
Public Quantity As Integer = 1

Public Sub New(ByVal node_object As T)
NodeObject = node_object
End Sub

Public Function AddChild(ByVal node_item As T) As NXTreeNode(Of T)
Dim child_node As New NXTreeNode(Of T)(node_item)

For Each tempNode As NXTreeNode(Of T) In Children
If tempNode.NodeObject.Equals(node_item) Then
tempNode.Quantity += 1
Return tempNode
End If
Next

Children.Add(child_node)
Return child_node

End Function

'Public Function RemoveChild(ByVal node_item As NXTreeNode(Of T)) As Boolean
' If Children.Contains(node_item) Then
' If node_item.Quantity > 1 Then
' node_item.Quantity -= 1
' End If
' If node_item.Quantity <= 0 Then
' Return Children.Remove(node_item)
' End If
' End If
' Return False
'End Function

Public Shadows Function ToString(Optional ByVal indent As Integer = 0) As String
Dim txt As String
txt = New String(" "c, indent) & NodeObject.ToString & " x" & Quantity.ToString & vbCrLf

For Each child As NXTreeNode(Of T) In Children
txt &= child.ToString(indent + 2)
Next child

Return txt
End Function
End Class

#End Region

*Note: the tree and node classes were adapted from:
http://www.java2s.com/Code/VB/Generics/DefineanduseagenericTreedatastruc...

Could you help me with such problem:

workPart.WCS.CoordinateSystem - will not work because NX says that this property is only available for DisplayPart.
So I can only get workPart.WCS.Origin but not the Orientation

Is there easy way to translate Orientation of WCS from Displaypart to Workpart?

One thing to keep in mind when working in the context of an assembly is that the display part controls the WCS. One way to get the orientation of the WCS from the display part to the work part is to save the WCS (menu -> format -> WCS -> save OR create a datum csys on the WCS), then change the work part to be the display part, the saved WCS represents the WCS of the assembly. Now you have a reference to the assembly WCS when working in the component part file.

Would it make sense to use WorkComponent.GetPosition (out Matrix3x3 orientation) and then get DisplayPart.WCS.CoordinateSystem.Orientation (NXMatrix) to calculate the Orientation of the WCS or this doesn't make sense?

I require the result of orientation in Matrix3x3 or NXMatrix

That sounds like a technique worth trying. Offhand, I can't see a reason why it wouldn't work.

At the moment your solution with saving WCS worked for me best. After I got what I need I delete it. Anyway, I am still afraid to create some temporary object in order not to leave something behind.

With builders it is quite easy to control what is created, but single commands like WCS.Save or workPart.CoordinateSystems.CreateCoordinateSystem(origin, orientation, False)

Makes me a bit afraid ... ;)

Thanks a lot for providing this code. I never would have come up with that. Although I'm not yet sure how to work with the result I'm looking into it. It seems the second parent shows the different in type after the first always is the Xform element but I'm not fully sure yet.

If you find some time to finish the code I would be excited to see the full result, but it is fantastic already.
BTW There seem to be a copy/paste mistake in your code at the beginning of the public class NXTree but after fixing that myself it runs as it should.

Is there a solution to create CSYS or Datum CSYS in the context of the assembly, that the CSYS is created in the WorkPart and not linked?
WCS coordinate system belongs to DisplayPart and when I try to use XForm to create Offset Datum CSYS I get the results linked to the assembly Part :(

You could remove the parameters of the XForm before creating the datum csys. This should make a datum csys that is not associated to anything else.

{XForm object}.RemoveParameters

I was using the code you posted in this tutorial. The one under "Example Code: Specify Csys".
I found a problem with .SetCoordinateSystemCartesianAtCsys when I have a part in the assembly active (workpart is not displaypart) and the workpart is moved/rotated inside the assembly.

When I use .SetCoordinateSystem insead then the WCS is placed correctly but I get the problem with Null Tag when I try to use Measure Dimension tool :/

How can I contact NXJournaling directly, so I could add an example assy?