Hello and Season's Greetings scripters! Needless to say I have been busy. More so than I anticipated. The API has been released and we are well on our way supporting your calls, building the websites, blogs, forums, examples, training documents, libraries, as well as finding the time to test, bug, enhance, plan, and innovate for the next release no rest for the weary. Ah but I do have a holiday break starting tomorrow

...so I guess I do get rest with my laptop of course I never leave home without it.

To the script! By design the code has been linear in flow, no classes, no loops, and no functions nothing too fancy. In this post we will finish it up and start digging into the heavier stuff. We will introduce inline macro calls (replaying recorded macros in script,) break out some of the tasks into functions, and in the end look ahead toward building a library file that can be imported into other python scripts running in our applications. We will also go over some features in our API and Python that make working in this environment a little easier.

The script, where we left it, had built the box and added the polygon mesh to the model manager. To catch up, if you haven't already done so, you can download the script from Part Two here.

OutOfTheBox_Image_010.png

Our next step will be to create the sphere and remove it from the volume of the box. We will use a slightly different approach in the construction. Since we do not have a cube primitive or Feature, we were forced to build the cube from scratch ( I sense an enhancement request.) However, we do have a SphereFeature. So we will create one and convert that to polygons. It's much less coding and as you will see, avaiable in other primitive shapes. To create code for the SphereFeature we'll again start with the API Help page for the SphereFeature object. You can search for it by typing SphereFeature in the search text box (found in the left column on any of the API Help pages.) Copying and pasting the needed code from the example at the bottom gives us the following snippet to start with:

# Create Sphere
sphere = SphereFeature()
sphere.center = Vector3D(0.0, 0.0, 0.0)
sphere.diameter = 1.0
sphere.axis = Vector3D(0.0, 0.0, 1.0)
sphere.radDir = Vector3D(0.0, 1.0, 0.0)
sphere.latitudeRange = Vector2D(0.0, 1.570796)

The variables in their default form are understandable: center, diameter, axis (of rotation or pole to pole), radDir (the direction, normal to the axis, at which the latitudinal sweep starts), and the latitudeRange (the start and stop angles in radians using the Right Hand Rule.) These angles can either can be + or - and range between zero and pi which brings up a good point, the 1.570796 can be substituted with math.pi (a constant from Pythons built in math module.) This constant will give you both a higher degree of accuracy and consistency across different platforms. Again the code from the API Help page is executable, so create a new script, copy and paste the entire example, tweak the numbers, and see what they do, if desired.

The next step is to convert that sphere to a polygon mesh. Currently the sphere is a Feature that does not have a parent object and is not visible in the Graphics Tab. Features when created through the GUI will be placed in the Features sub-folder under the active object in the Model Manager. The active object can be a polygon mesh, points or scan, the World, or even a CAD model. But this Feature (our feature) will only be used as construction geometry and does not need to be parented as it exists only"behind the scenes."

If you are curious about adding Features to objects, you could use the .addFeature call in geoapp here is the example code, Again, start a new wrap file, create a new Python script, paste, and run...if you haven't got the idea yet, you will...these examples are a great tool for learning and building your scripts!

import geoappall
for m in geoappall.execStrings: exec m in globals(), locals()

# Open test file
path = geoapp.appdata.Directories.getDirectory(geoapp.appdata.Directories.DIRECTORY_EXAMPLE_DATA)
geoapp.openFile(unicode(path +"\\features.wrp"))

# Get the currently selected model in the model manager.
activeModel = geoapp.getActiveModel()

# Create input plane
plane1 = PlaneFeature()
plane1.name = u"aPlane"
plane1.normal = Vector3D(1.0, 0.0, 0.0)
plane1.origin = Vector3D(0.0, 0.0, 0.0)

# Add feature to active object
geoapp.addFeature(activeModel, plane1)

So, much like the PlaneFeature in our last post, we will use the ConvertMeshFrom3DFeature to make our polygon sphere and then extract the mesh from the modifier's result parameter.

# Create the Sphere
sphere1 = SphereFeature()
sphere1.center = oPt
sphere1.radius = r
sphere1.axis = Vector3D(0.0, 0.0, 1.0)
sphere1.radDir = Vector3D(0.0, 1.0, 0.0)
sphere1.latitudeRange = Vector2D(0.0, math.pi)

# Convert the sphere to a mesh
createMeshFrom3DFeature = CreateMeshFrom3DFeature()
createMeshFrom3DFeature.feature = sphere1
createMeshFrom3DFeature.maxDeviation = maxDev
createMeshFrom3DFeature.run()

# Mesh that approximates the surface of the plane
pSphere = createMeshFrom3DFeature.mesh

All that is left to do is Boolean the sphere from the box. But, before we do that, let's start thinking about our library. Our script is getting longer and somewhat difficult to read. To help with this we should start encapsulating our tasks into functions. We can store these functions in a separate Python file that can be imported into (reused by) other Python scripts. If carefully thought-out, with your process environment in mind, these will save you an enormous amount of time in future work. This brings up topics on scope, relationships, and other issues we need to consider when working within our applications. We will be covering these in more detail in future postings.

Regarding functions, Python is an interpreted language and any imported modules or defined functions must be inserted after any imports, variables, classes, objects, etc. that are needed within the functions themselves and before where the functions are called. You will notice in the function below I have added a"centering" flag to change the placement of the box that is either to use the given point as the center or build the box in the positive space starting from that point. I also broke out the sphere into its own function. I put these after all my imports and before the main script that calls them.

def createPolyCube(s = 1.0, oPt = Vector3D(0,0,0), c = True ):
"""
createPolyCube - edge length, centroid
----------------------------------------------------------------
This function will create a tesselated cube aligned to the
World CSys with the
given edge length. The cube can
either be centered around the given point (c = True) or
started at the given point
and created in the positive x,
y, and z directions (c = False).

----------------------------------------------------------------
Variables
s (float) : Edge length of the box
oPt (Vector3D) : The origin point for the box placement
(defaults to World Origin)

c (Boolean) : Flag to center the box on given originPt
(defaults to True)
otherwise the box will
be created in positive space
starting at oPt
Devoluciones
pCube (geoapi.mesh.Mesh)
"""
#Centered?
if c:
cOrigin = Vector3D( oPt.x(), oPt.y(), oPt.z() + s/2 )
cURange = Vector2D(-s/2,s/2)
cVRange = Vector2D(-s/2,s/2)
else:
cOrigin = Vector3D( oPt.x(), oPt.y(), oPt.z() + s )
cURange = Vector2D(0,s)
cVRange = Vector2D(0,s)

# Create plane
pf1 = PlaneFeature()
pf1.normal = Vector3D(0.0, 0.0, 1.0)
pf1.origin = cOrigin
pf1.uRange = cURange
pf1.vRange = cVRange
pf1.uDir = Vector3D(1.0, 0.0, 0.0)

# Create and run a modifier to convert our plane to a mesh
createMeshFrom3DFeature = CreateMeshFrom3DFeature()
createMeshFrom3DFeature.feature = pf1
createMeshFrom3DFeature.run()

# Mesh that approximates the surface of the plane
pCube = createMeshFrom3DFeature.mesh

# create an edge selection
esel = EdgeSelection(pCube)
esel.markBoundary(True)

# extrude the boundary of our plane in the negative z direction
extrude = ExtrudeHeight(pCube)
extrude.height = s
extrude.direction = Vector3D(0, 0, -1)
extrude.closeExtrusion = True
extrude.run(esel.first())

# Deselect all triangles after the extrude
geoapp.clearActiveTriangleSelection(pCube)

return pCube


def createPolySphere(r = 0.5, oPt = Vector3D( 0, 0, 0 ), maxDev = .001 ):

"""
createPolySphere - radius, centroid
-----------------------------------------------------------------
This function creates a sphere feaure at the given origin
point and given radius, then
converts and returns it as a
polygon mesh using the maxDev as the tolerance for
tesselation

----------------------------------------------------------------
Variables
r (float) : Radius if the sphere
oPt (Vector3D) : The center point for the sphere
maxDev (float) : Specifies the tolerance for the
polygonal representation of the cylinder

Returns
pSphere (geoapi.mesh.Mesh)
"""
# Create the Sphere
sf1 = SphereFeature()
sf1.center = oPt
sf1.radius = r
sf1.axis = Vector3D(0.0, 0.0, 1.0)
sf1.radDir = Vector3D(0.0, 1.0, 0.0)
sf1.latitudeRange = Vector2D(0.0, math.pi)

# Convert the sphere to a mesh
createMeshFrom3DFeature = CreateMeshFrom3DFeature()
createMeshFrom3DFeature.feature = sf1
createMeshFrom3DFeature.maxDeviation = maxDev
createMeshFrom3DFeature.run()

# Mesh that approximates the surface of the plane
pSphere = createMeshFrom3DFeature.mesh

return pSphere

Please note the new syntax for my comments, in particular the triple quotes for the function summaries to support Python's inspect documentation string (__doc__) and I changed some of the object names, but for the most part it's still the same code. We do have to rewrite the actual script that calls these functions. I've also included the Boolean operation that subtracts the sphere from the cube by, once again, leveraging the API Help examples. Here is the first version of the main script body, not including all the imports and function definitions:

#
# Start Script
#

# local variables
cubeEdgeLen = 1.0
sDia = 1.38
cylDia = 0.5
toler = 0.0005
centered = True

#
# Create the Cube
#

myCube = createPolyCube( cubeEdgeLen, Vector3D(0,0,0), centered )

if isinstance (myCube, Mesh):
geoapp.addModel(myCube, u"OutOfTheBox-Cube")

# Remove the marked boundaries
geo.remove_all_boundaries()

#
# Create the Sphere
#
mySphere = createPolySphere( (sDia / 2), Vector3D(0,0,0), toler )

#
# Boolean sphere from the cube nesh
#
meshBoolean = BooleanMesh(myCube)
meshBoolean.meshModelAdd = mySphere
meshBoolean.operation = BooleanMesh.Subtract1
meshBoolean.run()

# Remove the marked edges
geo.remove_all_boundaries()

You should also note the inline geo.remove_all_boundaries macro call inserted after the Boolean operation and the use of Python's isinstance to test for successful object data form..and perhaps that I tightened the tolerance for conversion a little. This is probably a great time to talk about replaying recorded macros in script.

We have the ability to record operations in various formats (VBS, Python, etc.) and replay them from the Scripting Tab or from the Macro Group under Tools, or even from a custom button created with the Ribbon Customizer. Let's look at exectly how this is done...it's really quite intuitive.

OutOfTheBox_Image_010.png

When we created the box, the Extrude operation left behind some unneeded"marked edges" or (red) boundaries. We already deselected the marked triangles, but we have no way to remove these red edges or boundaries with the API today (I sense another enhancement request.) However, we are able to record a macro that can remove them and use that code within our script. This method uses our geo support module that is built dynamically in memory at application startup and is available to use within your scripts. Anything recorded in"geo." (gee oh dot) format when recording a macro, can be used inline with our script code. If you have ever recorded a macro you are probably already familiar with the geo. formatting. Here I use the Macro Record feature to remove the boundaries and then copy and paste the recorded code to our script. First, start recoding.

OOTB3_RecordMacro.PNG

Be sure to select Python for this example the syntax versus VBScript varies slightly.

OOTB3_RecordMacroPython.PNG

After pressing OK, you are in Record Mode and anything"Macro-able" will be sensitive (or selectable) if the current model, selections, and conditions are met for that command. For instace, if you have a point model selected the Wrap button in the ribbon will be sensitive, but Decimation would not. So, after running our final script from Part Two, let's remove the boundaries by selecting the box in the Model Manager (it should already be selected by default) and then under the Polygon Tab select the Remove All Boundaries command in the Boundaries Group's Remove command stack.

OOTB3_RemoveAllBound.PNG

Then go back to the Tools Tab and Stop the recording. You can find the newly created macro in the Macro List under the Scripting Tab. Open Script1 and select and copy the text.

OOTB3_Script1_CutAndPaste.PNG

Then simply paste in into our script after adding the box to the Model Manager.

OOTB3_RemoveAllBoundPasted.PNG

You should end up with something like this:

OOTB3_FinalGraphic.png

That about covers Part 3. The final OutOftheBox_Part3 script can be found here.

This last week I had some spare time away from my usual tasks and created the new library Python file (MyLibrary.py) and started to think more about our development environment and debugging. You can look ahead and see my work by downloading the OutOftheBox_Part4.py file, the new MyLibrary.py, and the Debug.py files.

Again, you will notice quite a few changes to the structure of the script, commenting, importing, a few new functions, and the use of namespaces when calling functions from geoapp or referencing the functions and vairables in MyLibrary.py, and Debug.py. Do not be alarmed. We will cover all these new features when we return to Part 4 in January.

Happy Holidays!

Richard