Tutorial 1: Fundamentals

This tutorial provides introductory information and terminology you should know before beginning to develop an ACIS-based application.

What Is ACIS?

ACIS is a geometric modeling kernel. It is not an end user application. It is a set of libraries that provide geometric modeling functionality to third party applications, similar to the way graphics libraries provide graphics capabilities to third party applications. ACIS has been implemented in C++ and its functionality is accessed through class member functions and global functions.

ACIS uses geometry and topology to represent the shapes of objects. It can represent models containing collections of volumes, stand-alone faces, and wire edges. Although people often talk about "solid modeling," ACIS volumes are not necessarily restricted to solids. For instance, a volume can represent the air surrounding a wing or the water flowing around a ship's hull. The ACIS model structure is described in more detail in Tutorials (Topology) and Tutorials (Geometry).

What Is the Scheme AIDE?

The Scheme AIDE (previously known as the ACIS 3D Toolkit) provides geometric modeling extensions to the Scheme interpreter. (Scheme is a dialect of the LISP programming language.) Like ACIS, it is not intended to be an end user application. It is, however, a mechanism to exercise a large subset of ACIS functionality using the Scheme language. The Scheme AIDE is easily extensible and can be used to test ACIS functionality, prototype software, or communicate questions or suspected problems to Spatial Corp. Because Spatial supplies customers with the source code to the Scheme AIDE, it also can serve as example code, demonstrating how ACIS functionality can be implemented within third party applications.

What Is an ENTITY?

If you were tasked with designing a geometric modeler, where would you start? You might start by defining what the model structure would be and then what operations a user could perform on the model. Skipping the model structure for a moment, let’s investigate some of the most basic operations. A list of basic operations might include:

1. One wants to construct a valid model.
2. One wants to delete a model or a portion of a model.
3. One wants to copy a model or a portion of a model.
4. One wants to save a model or a portion of a model to a file.
5. One wants to restore a model that was saved in a file.
6. One wants to "Undo" and "Redo" an operation or a portion of an operation.
7. One might even want to debug a model or a portion of a model.

If you were designing the geometric modeler using an object-oriented programming paradigm you might conceive of a base class that possesses this basic functionality. In ACIS this class is the ENTITY class. All of the classes used to represent the persistent geometry and topology of the model are derived from ENTITY. Therefore, all of these geometry and topology classes inherit this basic functionality.

To be a more precise, classes derived from ENTITY inherit the mechanisms for construction, deletion, copying, saving, restoring, undo/redo, debug, etc. from the ENTITY class, but the specific details of this functionality still must be implemented for each class (because the ENTITY class does not know the details of the classes derived from it.) The implementation of these member functions are aided by many macros and will be discussed later. Developers should be aware of a ramification of this: if the implementation for a derived class is not linked into an application, the derived class will not have full functionality in the application. This is generally not a problem, but you should be aware of it.

In addition, we should mention that names of classes derived from ENTITY are in UPPER CASE. Unfortunately there are also a few classes that are not derived from ENTITY whose names are in UPPER CASE. In addition, we should mention that some classes other than the basic geometry and topology classes are derived from ENTITY.

What Is a Tolerant ENTITY?

Occasionally, when models are imported from other geometric modeling systems there will be some geometric inconsistencies because the originating system used different tolerances than ACIS. For example, one might observe vertices that do not lie on edges to within the ACIS positional tolerance. These types of geometric inconsistencies do not typically occur in models that are constructed within ACIS.

One of the mechanisms ACIS uses to deal with such geometric inconsistencies are tolerant ENTITYs. There are two types of tolerant ENTITYs in ACIS: tolerant vertices and tolerant edges. Tolerant edges and vertices possess a local positional tolerance that is larger than the global positional tolerance. This allows these ENTITYs to deal with geometries that do not necessarily meet the ACIS positional tolerance. Tolerant ENTITYs are discussed in more detail in the technical article Tolerant Modeling.

Why Does One Need history?

If you have never written an application that uses geometric modeling, it may not be apparent why it is virtually essential to have an “Undo/Redo” mechanism. Even if the application developer does not expose this functionality to the end user, it is extremely valuable for exception handling. If something goes wrong during an operation that changes the model, you must "undo" the model to a known valid state. This is the primary value of the history mechanism. Exposing this capability to the end user is just an added benefit.

Given that a history mechanism is very valuable and one exists in ACIS, how can you use this mechanism in conjunction with exception handling? The simplest answer is: you use ACIS API functions. If you use ACIS API functions, exception handling is done for you and the "roll back" of the model occurs automatically. There is only one catch: most likely you will want to provide functionality for which there are no existing API functions. In these cases you can write your own API functions or mimic the behavior of ACIS API functions in your own functions.

The history mechanism in ACIS allows one to undo the model changes associated with an entire operation or a portion of an operation. Exception handling and the ACIS history mechanism are described in Tutorials (Exception Handling) and Tutorials (History Streams).

What Is an ACIS API Function?

There are two types of functions in ACIS: API functions and “Direct Interface” functions.

API functions:

• check the input arguments,
• contain exception handling code which prevent memory leaks from occurring within the API function,
• roll back changes in the model in the event of an error,
• return a status code (and in the event of an error possibly information about what went wrong), and

Direct Interface functions are lower level functions and may be either global functions or class member functions. Direct Interface functions generally do not check their input arguments, roll back the model in the event of an error, or return a status code. They do contain exception handling code to prevent memory leaks in the event of an error. Proper exception handling techniques are described in Tutorials (Exception Handling).

How Can One Attach Application-specific Data to an ACIS Model?

The primary mechanism to attach application-specific information to an ACIS model is by attaching attributes to ENTITYs. ENTITY attributes are implemented by deriving specific classes from the ATTRIB class. The ATTRIB class is derived from the ENTITY class; therefore, classes derived from the ATTRIB class will possess all the functionality of ENTITYs. In fact, ATTRIBs possess much more functionality than ENTITYs. For instance, one can specify the behavior desired when the owner of an ATTRIB is modified.

For a discussion of the derivation and use of user defined attributes see: Tutorials (Attributes}.

What Must Every ACIS Application Contain?

In addition to the header files and function calls one would expect to see in an application, every ACIS application must contain 3 ingredients:

1. ACIS licensing software,
2. An api_start_modeller / api_stop_modeller block, and
3. An api_initialize<component> / api_terminate<component> block.

Tutorials (Creating Applications) demonstrates these three ingredients in a simple ACIS application in both a Microsoft Visual Studio environment and a Linux environment. Subsequent tutorials will build upon these examples.

Note: ACIS licensing software was not required prior to ACIS version 17. Applications converted from versions prior to version 17, to versions 17 or higher, will need to add licensing software.

Tutorial 2: Creating Applications

This tutorial describes how to build a very minimal ACIS application using Microsoft's Visual Studio.

Create a "hello world" Application in Visual Studio

You should already be familiar with programming in your development environment; however, to illustrate the changes that must be made to create an ACIS application we will present a simple application first without ACIS linked into it and then again with ACIS linked into it. These instructions have been created using Microsoft Visual Studio 2005, version 8.0, but are generic so should apply to other versions as well. For the first non-ACIS application we shall use the following source file.

#include <stdio.h>

int main (int argc, char** argv) {
printf ("Hello, World!\n");
return 0;
}
1. Start Visual Studio.
One method is to open a new command window; execute vsvars32.bat; and then execute devenv.
2. Create a new Project.
1. Select File-New-Project.
2. Set the Project Type to be Visual C++ Win32 and Console Application.
3. Give it a unique name.
4. This will bring up a Wizard.
5. Select the Additional Option of Empty Project.
3. Create a new file to contain the main program using File-New-File. After creating the source file:
1. Save the source file.
2. Add the existing file to the project.
4. Build (that is, compile) the source file. Modify the source file and rebuild until no compilation errors exist.
6. Save everything using File-Save All.
7. Quit using File-Exit.

This procedure should be very familiar if you are familiar with programming in Microsoft's Visual Studio.

Create a Minimal ACIS Application

The primary changes needed to create an ACIS application are:

1. We need to define two environment variables, A3DT and ARCH, which are used during pre-compiling and linking.
2. We need to modify the definition of the PATH variable.
3. We need to include ACIS header files.
4. We need to call an application-specific ACIS licensing function. (University customers also need to register each installation of ACIS-based products. Refer to Registration.)
5. We need to start the modeler and initialize the necessary components.
6. We need to terminate all initialized components and stop the modeler.

A procedure to create an ACIS application using Visual Studio is given below.

1. Obtain a license file from Spatial's Downloads Center or contact Spatial’s Customer Support. Additional information on ACIS licensing is available at Application Licensing.
2. Open a new command window; set A3DT and ARCH; and then modify PATH.
1. A3DT is the path to the ACIS libraries.
2. ARCH is the architecture or platform (for example, NT_VC8_DLLD.)
3. Prepend %A3DT%\bin\%ARCH% to the existing PATH.
3. Execute vsvars32.bat and devenv as before.
4. Create a new project as before.
5. Select Project-Properties.
1. Under C/C++ General, Additional Include Directories add $(A3DT)\include. 2. Under C/C++ Preprocessor, Preprocessor Definitions add$(ARCH).
3. Under Linker General, Additional Library Directories add $(A3DT)\lib\$(ARCH).
4. There are several more directives listed in the on-line Installation documentation, but these should be sufficient for our minimal ACIS application.
6. Create a new source file.
7. Add the ACIS license file to the project directory and to the project. (This file includes example code for error handling that can be modified or commented out.)
8. Build, Execute, Save, Exit as before.

Discussion Regarding the Minimal ACIS Application

It is tempting to immediately start adding to this minimal application, but before we start doing that let's briefly look at this application.

The first ACIS related header file is acis.hxx. This header file should be included in all files that contain ACIS code. Moreover, it should precede all other ACIS header files and all application header files. It may come after system header files.

The only other ACIS header file that has been included is kernapi.hxx. This header contains declarations for a many of the most commonly used API functions in the ACIS kernel component.

The first ACIS related function call is api_start_modeller. api_start_modeller must be called before calling any other ACIS functions, with the exception of initialize_base. (Calling initialize_base is optional. It allows one to control the use of the free list and memory leak tracking subsystem. It even allows one to completely replace the ACIS memory management system. Whenever initialize_base is called before api_start_modeller, terminate_base should be called after api_stop_modeller. For more information on initialize_base, refer to base_configuration or Memory Management.) api_start_modeller should be called only once in an application.

We must call the ACIS licensing function after we have initialized the memory management system (which happened during our call to api_start_modeller.) If this is not done, ACIS functionality will not work.

In ACIS versions prior to R20, each ACIS component needed to be initialized separately (such as Blending and Local Ops). While these component initialization functions still exist and can be called, beginning in ACIS R20, all core ACIS components are initialized upon calling api_start_modeller. Correspondingly, api_stop_modeller handles the termination of all core ACIS components. Note that if your application still continues to make separate calls to the component initialization functions, your application must also have matching calls to the respective component termination functions.

However, non-core ACIS components must still be initialized individually, for example, PHL V5 and Defeaturing. For a description of which components are in which ACIS libraries, refer to ACIS Libraries and Library Initialization and Termination.

At this point ACIS is up and running. Congratulations!

After printing our "Hello, World!" message, we must properly shut down ACIS. This is done by terminating all the non-core components we initialized (none), and then calling api_stop_modeller. No ACIS function calls should come after api_stop_modeller. The only exception to this is terminate_base, which should only be called if you called initialize_base before api_start_modeller.

Enhance and Restructure the Minimal ACIS Application

In Tutorial 1, we mentioned that ACIS API functions return information regarding the success or failure of the API function. In the first implementation of our minimal application we ignored the objects returned by the API function calls. Let's modify our application to check the results of our API function calls. At the same time let's restructure the code to make subsequent tutorials easier to understand. Below is a revised source file for the minimal ACIS application. You should be able to simply replace the first version of the file with the revised version and rebuild your application.

In the latest version of our program, we examine the outcome returned by each API function call. An outcome contains an error number, among other things. In the event of an error (which should never occur with this simple program!), we print out the error number and its associated error message. Because of the seriousness of an error during start up or shut down, we quickly exit the application if an error occurs. Usually you will not exit your application when API functions produce errors. You may have noticed that my_initialization and my_termination are of type int, yet they return an err_mess_type. An err_mess_type is a typedef'd int. Handling exceptions is discussed in more detail in Tutorials (Exception Handling). This tutorial describes how to build a very minimal ACIS application in a Linux environment.

Create a "hello world" Application in Linux

You should already be familiar with programming in your development environment; however, to illustrate the changes that must be made to create an ACIS application we will present a simple application first without ACIS linked into it and then again with ACIS linked into it. For the first non-ACIS application, we shall use the following source file.

#include <stdio.h>

int main (int argc, char** argv) {
printf ("Hello, World!\n");
return 0;
}

One procedure for creating a simple, non-ACIS application is as follows:

1. Create a new text file to contain the main program using your favorite editor.
2. Build (that is, compile and link) the source file.
3. Modify the source file and rebuild until no compilation errors exist.
4. Execute the program by typing the name of the program.

Create a Minimal ACIS Application

The primary changes needed to create an ACIS application are:

1. We need to create or modify the sample makefile for the application.
3. We need to call an application-specific ACIS licensing function.
4. We need to start the modeler and initialize the necessary components.
5. We need to terminate all initialized components and stop the modeler.

Note: If the application is not going to run on the Microsoft Windows operating system, an ACIS licensing function is not required; however, to create a source file that is platform-independent, we leave the licensing functionality in this demo application. The ACIS licensing function can be made platform-specific by adding an #ifdef _MSC_VER block to it. The use of ACIS on non-Windows platforms is controlled contractually.

One procedure to create an ACIS application using Linux is given below. In this example, we use a makefile to capture the information needed to compile and link our application.

1. Obtain a license file from Spatial's Download Center or contact Spatial’s Customer Support, if the application will run on Windows as well as Linux. Additional information on ACIS licensing is available at Application Licensing.
2. Create a new source file.
3. The sample makefile should be edited to reflect:
1. The correct location of your source file(s), ACIS header files, and ACIS libraries
2. The name of the source file(s) and
3. The name of your license file. (The license file includes example code for error handling that can be modified or commented out.)
4. Build (that is, compile and link) the source file using the make utility.
5. Modify the source file and rebuild until no compilation errors exist.
6. Execute the program by typing the name of the program.

If your executable cannot find the ACIS shared libraries, you may need to tell it where to look. On Linux, the path to ACIS shared libraries may be specified by appending the appropriate path to your LD_LIBRARY_PATH environment variable. The way to do this depends on which type of shell you are using. With the C Shell (csh) you will type something similar to the following:

 setenv A3DT <location of ACIS pool>
setenv ARCH <linux_so or linux_so_debug>
setenv LD_LIBRARY_PATH "${LD_LIBRARY_PATH}:$(A3DT)/lib/(ARCH)"  Discussion Regarding the Minimal ACIS Application It is tempting to immediately start adding to this minimal application, but before we start doing that let's briefly look at this application. The first ACIS related header file is acis.hxx. This header file should be included in all files that contain ACIS code. Moreover, it should precede all other ACIS header files and all application header files. It may come after system header files. The only other ACIS header file that has been included is kernapi.hxx. This header contains declarations for many of the most commonly used API functions in the ACIS kernel component. The first ACIS related function call is api_start_modeller. api_start_modeller must be called before calling any other ACIS functions, with the exception of initialize_base. (Calling initialize_base is optional. It allows one to control the use of the free list and memory leak tracking subsystem. It even allows one to completely replace the ACIS memory management system. Whenever initialize_base is called before api_start_modeller, terminate_base should be called after api_stop_modeller. For more information on initialize_base, refer to base_configuration or Memory Management.) api_start_modeller should be called only once in an application. If the application is intended to run on Microsoft Windows as well as Linux, we must call the ACIS licensing function after we have initialized the memory management system (which happened during our call to api_start_modeller.) If this is not done, ACIS functionality will not work. (ACIS Licensing is required for all ACIS applications running on Microsoft Windows, but not for other platforms.) In ACIS versions prior to R20, each ACIS component needed to be initialized separately (such as Blending and Local Ops). While these component initialization functions still exist and can be called, beginning in ACIS R20, all core ACIS components are initialized upon calling api_start_modeller. Correspondingly, api_stop_modeller handles the termination of all core ACIS components. Note that if your application still continues to make separate calls to the component initialization functions, your application must also have matching calls to the respective component termination functions. However, non-core ACIS components must still be initialized individually, for example, PHL V5 and Defeaturing. For a description of which components are in which ACIS libraries, refer to ACIS Libraries and Library Initialization and Termination. At this point ACIS is up and running. Congratulations! After printing our "Hello, World!" message, we must properly shut down ACIS. This is done by terminating all the non-core components we initialized (none), and then calling api_stop_modeller. No ACIS function calls should come after api_stop_modeller. The only exception to this is terminate_base, which should only be called if you called initialize_base before api_start_modeller. Enhance and Restructure the Minimal ACIS Application In Tutorial 1 we mentioned that ACIS API functions return information regarding the success or failure of the API function. In the first implementation of our minimal application we ignored the objects returned by the API function calls. Let's modify our application to check the results of our API function calls. At the same time let's restructure the code to make subsequent tutorials easier to understand. Below is a revised source file for the minimal ACIS application. You should be able to simply replace the first version of the file with the revised version and rebuild your application. In the latest version of our program we examine the outcome returned by each API function call. An outcome contains an error number, among other things. In the event of an error (which should never occur with this simple program!), we print out the error number and its associated error message. Because of the seriousness of an error during start up or shut down, we quickly exit the application if an error occurs. Usually you will not exit your application when API functions produce errors. You may have noticed that my_initialization and my_termination are of type int, yet they return an err_mess_type. An err_mess_type is a typedef'd int. Handling exceptions is discussed in more detail in Tutorials (Exception Handling). Tutorial 3: Topology ACIS Topology The basic concept of a boundary representation model is the topology describes how elements are bounded and connected; the geometry describes the shape of each individual element. A rather thorough description of the ACIS topological structure is provided in Topology. You should familiarize yourself with that description before continuing with this tutorial, because this tutorial will assume you understand that information. Topology tells us what is adjacent to what. For example, the topology may tell us that an edge, E1, is bounded by vertices V1 and V2. If we also know that another edge, E2, is bounded by vertices V2 and V3, then we know that edges E1 and E2 are adjacent because V2 bounds both edges. This is depicted in the first diagram below. Similarly if a face, F1, is bounded by edges E1, E2, E3, and E4, and another face F2, is bounded by edges E1, E5, E3, and E6, then we know that faces F1 and F2 are adjacent because both faces are bounded by E1 and E3. This is depicted in the second diagram below. Much of the adjacency information in the topological data structure is obtained by traversing the data structure. Is it readily apparent that the topology in the middle diagram above with four vertices, six edges, and four faces is the topology for the object shown on the right? Do you see that this also could be the lower topology for a cylinder? Or a frustum of a cone? Or a sphere? Bodies A body is a collection of one or more lumps. Most ACIS algorithms operate on bodies, so generally when we construct ACIS models we will create bodies, rather than stand-alone lower topological entities. The concept of a body is implemented in the BODY class. The BODY class is derived from the ENTITY class. Each instance of a BODY contains a pointer to the first LUMP in a singly-linked list of LUMPs. Lumps A Lump Containing Multiple Volumes, Sheets, and Wires A lump is a set of connected points. It may consist of volumes, sheets, and/or wires. For a volume the lump includes not only the points on the boundary of the volume, but also the points in the interior of the volume. For sheets and wires there is no interior; so they consist strictly of the points on the boundary. The figure to the right depicts a lump containing multiple volumes, sheets, and wires. A lump is analogous to a lump of clay. It can be a thick lump, that is, a volume, or it can be squished flat into a sheet, or it can be rolled into a thin wire, or it can consists of many volumes, sheets, and wires. The primary constraint is that all the pieces of clay must be connected. What are the relationships among the lumps of a body? The lumps of a body should not be connected to each other physically or topologically. If two lumps touch each other, they should be combined into a single lump. Why do we need lumps? Could not each body represent a single lump, in which case the body would simply contain a list of shells? The simple answer is, lumps are not necessary. You could design a topological structure without them. They have been included in the ACIS topological structure for efficiency. Many algorithms are simpler and more efficient because of the presence of lumps and many interfaces are simpler because of the presence of lumps. At their discretion algorithms may separate a body with multiple lumps into separate bodies, or they may combine bodies with physically separate lumps into a single body with multiple lumps. The concept of a lump is implemented in the LUMP class. The LUMP class is derived from the ENTITY class. Each instance of a LUMP contains a pointer to the BODY that owns it, a pointer to the next LUMP in the linked list of LUMPs owned by the BODY, and a pointer to the first SHELL in a singly-linked list of SHELLs. Shells A Solid Body with an Interior Void A shell is a connected set of boundary elements. For sheet bodies and wire bodies, there is a one-to-one correspondence between lumps and shells. In other words, a sheet body or wire body with a single lump will have a single shell. For solid bodies, there can be more than one shell per lump. A lump possesses multiple shells when there is an interior void in the volume. For example, if one had a hollow rubber ball, one shell would be used to represent the outer spherical surface and another shell would be used to represent the inner spherical surface. Note, these two spherical shells are not connected to each other. Their only connection is through the lump structure. The only time when multiple shells exist in a lump is when there are interior voids in a volume. The figure at the right depicts a solid with an interior void, which is represented by a lump with two shells. What are the relationships among the shells of a lump? The shells of a lump should not be connected to each other physically or topologically. If two shells touch each other, they should be combined into a single shell. Why do we need shells? Could not each lump represent a single shell, in which case the lump would simply contain a list of faces and wires? The shell construct allows us to have voids in a volume. If shells did not exist, you could not have two disconnected sets of faces in a solid body. They would have to be connected to each other somehow. The cleanest approach is simply to implement the shell concept. The concept of a shell is implemented in the SHELL class. The SHELL class is derived from the ENTITY class. Each instance of a SHELL contains a pointer to the LUMP that owns it, a pointer to the next SHELL in the linked list of SHELLs owned by the LUMP, a pointer to the first FACE in a singly-linked list of FACEs, and a pointer to the first WIRE in a singly-linked list of WIREs. In addition, a SHELL may contain a pointer to a hierarchy of SUBSHELLs. Subshells A shell typically consists of faces and wires. Occasionally a shell will be subdivided into a hierarchy subshells, but this is rare. A subshell, similar to a shell, is a connected set of faces, wires, and subshells. Unlike shells, the constituents of two subshells may be connected to each other. Subshells exist in the topological data structure as an efficiency tool for algorithms. At their discretion algorithms may create, expand, or collapse a hierarchy of subshells. When would you want to use subshells? If you had a complex model (for example, on the order of thousands of faces or tens of thousands of faces) and you knew that subsequent modeling operations were going to be restricted to a relatively small spatial region, you could subdivide a shell into two pieces: the portion of the shell inside the region of interest and the portion of the shell outside the region of interest. Then subsequent modeling operations could relatively easily exclude the portion of the shell that was outside the region of interest. Alternatively, proponents of octree-based methods could subdivide a shell based upon an octree-based algorithm. The diagram below depicts a shell that has been decomposed into a hierarchy of subshells. You might use such a hierarchy to represent a binary tree or octree decomposition. In this example, the leaves of the tree would most likely contain all the face and wire lists. A Shell Decomposed into a Hierarchy of Subshells The concept of a subshell is implemented in the SUBSHELL class. The SUBSHELL class is derived from the ENTITY class. Each instance of a SUBSHELL contains a pointer to its parent SUBSHELL, a pointer to the next SUBSHELL in the linked list of SUBSHELLs of its owner (that is, its next sibling), a pointer to the first FACE in a singly-linked list of FACEs, a pointer to the first WIRE in a singly-linked list of WIREs, and a pointer to a hierarchy of child SUBSHELLs. A FACE or WIRE must be contained either in the SHELL itself or in exactly one of its SUBSHELLs. Wires A Body with One Solid and Three Wire Regions A wire is a set of connected edges that do not bound faces. Wires may contain one or more edges, be open or closed, have branches, and have multiple circuits. In addition, a wire may be either outside or inside a volume. If the wire is inside a volume, it can be interpreted as an infinitesimally small hole through the volume. Alternatively, wires contained within a volume could be used to represent a non-homogeneous, engineered material, such as concrete reinforced with rebar. A wire cannot be both inside and outside a volume. A body with two external wires and one internal wire are depicted in the figure to the right. The diagram below depicts the edges and coedges of a wire with multiple branches and one closed circuit. Notice that the direction of the coedges may be reversed with respect to the edges. Edges and Coedges of a Wire What are the relationships among the wires of a shell? The wires of a shell should not be connected to each other physically or topologically unless they touch at a vertex on a face. In any other situation, if two wires touch each other, they should be combined into a single wire. In addition, wires must be split wherever they intersect a face - and each wire must be attached to a vertex on the face at the point of intersection. The concept of a wire is implemented in the WIRE class. The WIRE class is derived from the ENTITY class. Each instance of a WIRE contains a pointer to the SHELL that owns it, a pointer to the next WIRE in the linked list of WIREs owned by the SHELL, and a pointer to the first COEDGE of the WIRE. (The relations among the COEDGEs of a WIRE are discussed in the Coedges section below.) The topological elements of most interest are faces, edges, and vertices. The previously mentioned upper topological elements (bodies, lumps, shells, subshells, and wires) are simply means of organizing sets of faces, edges and vertices. Faces, edges, and vertices have corresponding geometry elements (surfaces, curves, and points) which specify the shape of the object we are modeling. Faces A face defines a bounded region on a surface. It allows us to model using a small portion of a much larger surface and to model regions with holes in them. A Body with One Solid and Three Sheet Regions A face may be a sheet face, in which case it is exterior to all solid regions, or a face may bound a solid region, in which case it separates the inside of the solid from the outside of the solid (in other words it has material on only one side of it), or a face may be embedded inside a solid region, in which case it has material on both sides of it. A body with a solid region, two external sheet faces, and one internal face is depicted in the figure to the right. A face may be bounded by 0, 1, or more loops of edges. A face may have zero bounding loops if the surface of the face is bounded. The only two cases in which this occurs are: (1) if the face is a complete sphere and (2) if the face is a complete "donut" torus. (If the surface is an "apple" or "lemon" torus, then vertices exist at the surface singularities.) If a face has a single loop, the loop runs around the periphery of the face and the face does not have any holes in it. Usually if a face has multiple loops, it has holes in it, although a loop may degenerate to a single point, in which case the hole is infinitesimally small. A common example of a face with two loops, one of which that has degenerated to a single point is a conical face, where the degenerate loop is at the apex of the cone. What are the relationships among the faces of a shell? Unlike wires, the faces of a shell may be connected to each other both physically and topologically. If two faces touch each other (that is, they intersect), then there must be one or more edges and vertices to represent the set of common points. Faces of a shell may not touch except along their boundary edges and vertices. The concept of a face is implemented in the FACE class. The FACE class is derived from the ENTITY class. Each instance of a FACE contains a pointer to the SHELL that owns it, a pointer to the next FACE in the linked list of FACEs owned by the SHELL, a pointer to the first LOOP of the FACE, and pointer to the SURFACE underlying the FACE. (SURFACEs are discussed in Tutorials (Geometry).) Loops A loop consists of one or more coedges and is used to bound a face. In other words, a loop of coedges separates the region that is inside the face from the region that is outside the face. (Loops do not exist in wires.) The direction of a loop of coedges is counter clockwise with respect to an outward pointing face normal. (For those of you with a background in physics or mathematics, this follows the "right hand rule." If you think of the thumb of your right hand as the normal to the face, your curled fingers point in the direction of the loop.) Alternatively, one may view the orientation of a loop of coedges as always having the material of the face on the left side of the coedge. Coedges are always oriented "head to tail" in a loop. In other words, the coedges of a loop form a closed loop. (Well, almost always... ACIS can model unbounded faces or partially bounded faces, but this is very rarely done. Faces are generally bounded, which implies that loops are closed.) Multiple loops are required on a face if there are holes in the face. (This is analogous to a lump needing multiple shells if there are voids in the lump.) Some ACIS developers want to classify all loops as being either peripheral loops or hole loops. That is, they want to determine if the loop bounds the exterior of the face or a hole within the face. It is not always possible to make this determination. The notions of "interior" and "exterior" loops depend upon the geometry of the face and is not a topological quantity. For instance, given a spherical face with two relatively small loops on it, how can one determine which loop represents the periphery of the face and which loop represents a hole in the face? Faces with Various Types of Loops ACIS does contain a loop classification algorithm. What does it do when confronted with such situations? In the first image in the figure to the right, the planar face has a single loop that could be classified as a peripheral loop, and in the second image in the figure to the right, the planar face has one loop that could be classified as a peripheral loop and one loop that could be classified as a hole loop. But what about the two loops on the cylindrical face in the third image? Neither of these is obviously peripheral to the other. So what does ACIS do? ACIS classifies a loop as being of one of three types: a peripheral loop, a hole loop, or a separation loop. So, what is a separation loop? A separation loop exists on a surface that is closed in one or both directions, the loop completely spans the parameter range in the closed direction, and it separate the surface into two regions. Actually, ACIS distinguishes between loops that separate the surface in the parametric u direction, the parametric v direction, and in both parametric directions. The two loops in the third image are examples of u-separation loops. They wrap completely around the cylinder. Cylindrical, conical, spherical, toroidal faces can are closed in the parametric v direction and can be bounded by u-separation loops. Toroidal faces are also closed in the parametric u direction so they can be bounded by v-separation loops. The next two images in the figure depict toroidal faces with u- and v-separation loops. Of course, faces with separation loops can also have hole loops. The final two images in the figure depict toroidal face with several hole loops in addition to the separation loops. ACIS may also classify a loop on a face that is closed in both the u- and v-parametric directions as a uv-separation loop if it divides the surface in both the u- and v- directions. Starting in ACIS R16, spline surfaces can (optionally) be represented without a seam edge. This is controlled by the periodic_no_seam option. We will discuss this option more in Tutorials (Geometry) when we discuss spline surface geometry, but for now we are just interested in the effect of this option on loops. If spline surfaces require a seam edge, then spline faces will have only peripheral and hole loops. They cannot have separation loops. If spline surfaces do not require a seam edge, then spline faces may also have separation loops. What are the relationships among the loops of a face? The loops of a face should not be connected to each other physically or topologically. If two loops touch each other, they should be combined into a single loop. This implies that a loop may have non-manifold regions in it. In other words, loops are not separated at non-manifold vertices. Why do we need loops? Could not each face contain a list of coedges, thereby eliminating the need for a loop structure? A loop is analogous to a shell. Just as a shell allows us to have voids in a volume, a loop allows us to have holes in a face. If loops did not exist, you could not have two disconnected sets of edges in a face. They would have to be connected to each other somehow. The cleanest approach is simply to implement the loop concept. The concept of a loop is implemented in the LOOP class. The LOOP class is derived from the ENTITY class. Each instance of a LOOP contains a pointer to the FACE that owns it, a pointer to the next LOOP in the linked list of LOOPs owned by the FACE, and a pointer to the first COEDGE of the LOOP. Coedges Coedges represent the use of an edge by upper topology. (Because ACIS has coedges, it can represent regions that are the non-manifold along an edge. Conversely, if each edge were restricted to being used twice by faces, as in a traditional winged-edge data structure, then only 2-manifold regions could be modeled.) If an edge is used by a wire, then the edge will have a single coedge. If an edge is used by one or more faces, it will have a coedge for each use by each face. If an edge is used twice by a face, then the face is on both sides of the edge. There are three terms for edges that are used twice by a face that you should be aware of. Faces with Spur, Prop, and Seam Edges • A spur edge is an edge that is not closed and it does not connect to another edge on one or both ends. In other words, on one or both ends it has a free vertex or a spur vertex. Spur edges can also exist in wires. • A prop edge is an edge that is not closed that is used twice by a face and is not a spur edge. It has adjacent edges at both ends. The exception to this if one or both of the vertices lie at singularities. A spur edge may have a vertex at which there are no adjacent edges if the vertex lies at a surface singularity. Prop edges and spur edges are mutually exclusive. • A seam edge is a special case of a prop edge. If a prop edge runs along the parametric boundary of a periodic surface (that is, it splits the face along parametric boundary of a periodic surface), then it may be called a seam edge. In general, spur edges and prop edges are not necessary for the topology of a face and may be removed by merging. The exception to this is a seam edge on a spline surface. As was stated in the loop discussion, prior to ACIS R16, periodic faces on periodic spline surfaces required seam edges. Since ACIS R16, seam edges on periodic spline surface are optional. The concept of a coedge is implemented in the COEDGE class. The COEDGE class is derived from the ENTITY class. Each instance of a COEDGE contains a pointer to the LOOP or WIRE that owns it, a pointer to the next COEDGE owned by the LOOP or WIRE, a pointer to the previous COEDGE owned by the LOOP or WIRE, a pointer to the partner COEDGE, a pointer to the underlying EDGE, and in some cases, a pointer to parameter space curve, a PCURVE. (PCURVEs are discussed in Tutorials (Geometry).) Edges An edge is either part of a wire or used to bound a face. In other words, if an edge is part of a wire, it cannot bound a face and similarly if an edge bounds a face, it cannot be part of a wire. A single edge can bound many faces. The faces bounded by an edge are obtained from the coedges of the edge. Each coedge represents the use of an edge by a face (or a wire.) The coedges of an edge are stored in a counter clockwise order about the edge; therefore, we can obtain an ordered list of faces about the edge. If an edge belongs to a wire, it will have a single coedge. (The exception to this rule is an "intersection graph" used by the ACIS Boolean algorithm. An edge in an intersection graph wire may have multiple coedges. The structure and use of intersection graph wires is beyond the scope of this tutorial. You should just be aware that if you obtain a intersection graph wire, it will contain more than one coedge per edge and needs to be "cleaned" before it can be used by other ACIS algorithms.) You obtain adjacent edges in a wire by following the pointers in each edge's coedge. An edge has a direction. The direction of the edge is obtained using the direction of its underlying geometry; that is, its curve. The edge may run in the same direction as its curve, or in the opposite direction of its curve. Because an edge is directed, it has a start and an end vertex. These may be the same vertex. In addition to the three types of edges mentioned in the Coedges section (spur, prop, and seam edges), there are a few more types of edges that you should be familiar with. Edges can be categorized according to their closure. For instance, if an edge has two distinct vertices, then the edge is open. If the edge start and end vertices of an edge are the same, the edge is closed. If the edge is closed and underlying curve's start and end tangent directions are the same, then the edge is periodic. Another special type of edge is one with zero length. Such an edge is considered to be a degenerate edge. Such an edge is often referred to as a NULL edge because the underlying pointer to the edge's geometry is NULL. One should never encounter a NULL edge in the interior of a wire, although a wire may consist entirely of a NULL edge. If a NULL edge resides in the middle of a face, it is often referred to as an isolated vertex. You would obtain an isolated vertex if you connected two spheres that were touching at a single point. There is also an isolated vertex at the apex of a cone and at the singularities on degenerate tori. Isolated vertices are not required at the poles of spheres. What are the relationships among the edges of a face or wire? The edges of a face or wire may be connected to each other both physically or topologically. If two edges touch each other (that is, they intersect) then there must be one or two vertices to represent the set of common points. Edges of a face or wire may not touch except at their boundary vertices. The concept of an edge is implemented in the EDGE class. The EDGE class is derived from the ENTITY class. Each instance of a EDGE contains a pointer to the COEDGE that owns it, a pointer to the start VERTEX of the EDGE, a pointer to the end VERTEX of the EDGE, and pointer to the CURVE underlying the EDGE. (CURVEs are discussed in Tutorials (Geometry).) Vertices A vertex typically is used to bound an edge. In fact, a vertex may bound many edges. In a manifold solid, the vertex will point to one of its edges and all of the other edges bounded by the vertex can be obtained by traversing edge and coedge pointers. This is the most common case. A Non-manifold Vertex Connecting Two Separation Surfaces If a volume is non-manifold at the vertex, as shown in the figure to the right, then the vertex will point to one of the edges it bounds in each manifold region (also called a "separation surface"). The other edges in each separation surface that are bounded by the vertex can be obtained by traversing edge and coedge pointers. (The pointers of the edges and coedges connected to the non-manifold vertex are not affected by the non-manifold vertex.) If a vertex bounds an edge of a face (including a NULL edge in the interior of a face) and an edge of a wire, the vertex points to the edge on the face and the edge on the wire. In other words, the wire is treated as if it were a separation surface. Looking at this from another perspective, if a wire touches the interior of a face, there must be a vertex at the point where the wire and face intersect. The vertex in the face will be an isolated vertex (that is, a NULL edge). The vertex must contain a pointer to both an edge of the wire and the NULL edge of the face. What are the relationships among the vertices of a face or wire? The vertices of a face or wire must be physically distinct from each other. If two vertices touch each other (that is, they are within a global or local tolerance of each other), then the vertices must be combined into a single vertex. The concept of a vertex is implemented in the VERTEX class. The VERTEX class is derived from the ENTITY class. Each instance of a VERTEX contains a pointer to an EDGE that owns it (or in the case of a non-manifold VERTEX, a list of pointers to a set of EDGEs that own it), and pointer to the APOINT underlying the VERTEX. (APOINTs are discussed in Tutorials (Geometry).) Topology Classes The topological classes in ACIS are derived from the ENTITY class. What do we already know about these classes simply because they are derived from the ENTITY class? First, we know that they are persistent; that is, they have states that are recorded in the ACIS history structure. They may be created, deleted, modified, saved, restored, copied, and debugged. And we know that the topology classes have UPPER CASE names. They are, from the top-down, BODY, LUMP, SHELL, SUBSHELL, WIRE, FACE, LOOP, COEDGE, EDGE, VERTEX. Each topological class is defined in its own header file (for instance, the BODY class is defined in body.hxx), but for your convenience all of these classes are defined in the header file alltop.hxx. Manifold and Non-manifold Objects In this tutorial and elsewhere in ACIS documentation, we frequently use the terms manifold or non-manifold. What do these terms mean? A manifold is a mathematical term for an object that locally resembles a line, a plane, or a space. The term n-manifold refers to a manifold that locally resembles a n-dimensional space, not one which lies in an n-dimensional space. In our geometric modeling context, we typically refer to 2-manifold boundaries of objects. Having a 2-manifold boundary means that each point on the boundary is like a point on the interior of a plane. Each point on the 2-dimensional boundary is completely surrounded by other points on the 2-dimensional boundary. The surface of a sphere, a torus, or a block are examples of 2-dimensional manifolds. (In this context the "surface" of an object refers to its exterior boundary, not a specific geometric entity.) Each point on the boundary of a manifold solid divides the modeling space into two regions: the region inside the solid and the region outside the solid. If at any point on the boundary the modeling space is not divided into two regions, then the object is non-manifold at that point. Three Non-manifold Objects A non-manifold point is depicted in the top image in the figure to the right. At the non-manifold vertex, there are two "inside" regions. You can go in one of two directions and go "into" the object. Looking at this from a different perspective, there are two manifold boundary surfaces that meet at the non-manifold point. These are the "separation surfaces" we referred to above. A non-2-manifold region can be larger than a single point. The region can extend along a curve. For example, two blocks may share an edge. This is depicted in the middle image in the figure to the right. Along the edge, the boundary of the object is non-2-manifold. Space is not divided into an "inside" and "outside" region at any given point along the edge. At any point along the edge, there are two directions you can travel to go into the object. Similarly, the non-2-manifold region can extend along a surface. For example, imagine a block with an internal face. Space is not divided into an "inside" and "outside" region at any given point along the internal face. At any point along the internal face there are two directions you can travel to go into the object. Can a face have a non-manifold region on its interior? Yes. The bottom image in the figure to the right depicts a planar face with two circular holes aligned such that they just touch each other. The face is non-manifold at the point where the two circular holes touch. At this point, there are two directions into the interior of the face. (We will discuss the specific topology of this case later in this tutorial.) Being manifold should not be confused with orientable. There are objects that are manifold that are not orientable, for instance, a Möbius Strip or a Klein bottle. (If a surface is orientable, that means that directions are consistent across the surface. If one defines a direction, for instance clockwise, at a given point A, and one travels along a path to point B, thereby establishing a consistent clockwise direction at point B. The surface is orientable if for all paths on the surface between A and B the direction of clockwise is consistent.) Being manifold should also not be confused with being realizable. While it true that non-manifold objects are not realizable, there are lots of objects that are not realizable that are manifold. Solid and Sheet Topology The principal difference between solids and sheets is the "sidedness" of the faces. (Solids are also typically "closed" but ACIS does allow you to model open solids. You should use caution if you model with open solids. Operations on them may not produce the results you would expect.) If a set of faces bound a solid region, they are marked as having material on one side of them. If the faces exist inside a solid region, they are marked as having material on both sides of them. If they exist outside all solid regions, they are marked as having no material on either side of them. Another major difference between solids and sheets is that the faces comprising the exterior boundary of a solid region must have a consistent orientation. The convention in ACIS is for the normals of all faces bounding a solid region to point outward, away from the material of the solid. Faces of a sheet are not required to have a consistent orientation. The topological structure of solids and sheets is rather straight-forward above the face level. Bodies point to a list of lumps, each lump points to a list of shells, and each shell points to a list of faces. In addition to each shell maintaining a list of its faces, the topological structure describes how the faces are arranged; that is, what is next to what. This connectivity information is why loops, coedges, edges, and vertices exist. If we just had geometric entities (for example, trimmed faces), we would not have an explicit representation of how the faces were connected. The boundary representation in ACIS allows us to determine which edges and vertices bound a face and how they are connected. It allows us to determine which faces radiate from an edge and which vertices bound an edge. And it allows us to determine which edges and faces surround a vertex. Putting this information together allows us to determine which faces are adjacent to a given face, which edges are connected to a given edge, or what vertices are adjacent to other vertices. These adjacency relationships can be summarized in the following table, where F stands for Face, E stands for Edge, and V stands for Vertex. A geometric modeler must be able to determine all of these relationships.  F : {F} F : {E} F : {E} E : {F} E : {E} E : {V} V : {F} V : {E} V : {V} Coedge Next and Previous Pointers As we stated previously in this tutorial, each loop of a face must be distinct. They cannot touch each other. If what appears to be two loops touching, they are actually one loop. This helps us to understand the topological structure at a non-manifold vertex on a face. We will discuss this shortly, but first let's look at what happens in a typical manifold face. A Face with Two Loops of Coedges As we also stated previously in this tutorial, the coedges form a loop around boundary of the face such that the material of the face is always on the left side of the coedge. Each coedge in the loop points to the next and previous coedges in the loop. You should realize that the direction of the coedge may be the same as the direction of the edge, or it may be in the opposite direction of the edge. The notion of next and previous are with respect to the direction of the coedge, not the direction of the edge. A diagram depicting a face with two loops of coedges is show to the right. Observe how one can follow the next coedge pointers to traverse around either loop in the forward direction, or how one can follow successive previous pointers to traverse in the reverse direction. Six Faces Containing Two Loops of Four Edges One should avoid drawing conclusions about the shape of a face from its topological representation. For instance, each of the six faces shown in the figure to the left (with planar, cylindrical, spherical, conical, and toroidal surfaces) possess the same topology; that is, the topology depicted in the diagram above. A Face Containing a Non-manifold Vertex What happens at a non-manifold vertex in a face? Earlier in this tutorial we described a planar face with two circular holes that were aligned such that the holes just touched each other. (You could also describe this face as having a single hole that is shaped like two circular holes that intersect at a single point.) The diagram to the right depicts the topology for such a face. This face has two loops of coedges. The peripheral loop of coedges is identical to what we have seen previously. Notice the connectivity of the coedges around the inner loop of coedges. As before, we can follow the next coedge pointers to traverse around the loop in the forward direction, or we can follow the previous pointers to traverse around the loop in the opposite direction. We should also point out that the non-manifold vertex points to a single edge on the face. Why is that? Because there is a single separation surface. The vertex does not need to point to additional edges because all the edges can be obtained by traversing the coedge pointers about the vertex. A Face with an Isolated Vertex What happens at an isolated vertex in a face? The diagram to the left depicts the topology of a face with an isolated vertex. The face has two loops of coedges. The peripheral loop of coedges is identical to what we have seen previously. The inner loop has a single coedge. The next and previous coedges pointers of this coedge point to itself. It is a loop of one coedge. If a face had contained a finite sized hole surrounded by a single closed edge, the inner loop also would have contained a single coedge whose next and previous coedges pointers pointed the coedge. In other words, the topology would have been the same! In the isolated vertex case, however, the edge has zero length and does not have an underlying curve. Four Faces Meeting at a Vertex Up to this point, we have depicted coedges lying on a single face. Let's look at coedges lying on adjacent faces. The diagram to the right depicts the coedges on four faces of a sheet or solid that meet at a common, manifold vertex. Each of the manifold edges has two coedges. Each coedge that is directed into the common vertex will be linked (by its next pointer) to the coedge on its face that is directed away from the vertex. Each coedge that is directed away from the common vertex will be linked (by its previous pointer) to the coedge on its face that is directed into the vertex. Thus, the coedges on each face form doubly-linked lists of coedges on each face. This diagram is very symmetric about the common vertex. How would this diagram be different if there were only three faces and one of the edges was a spur edge or a prop edge? There would be no differences in the next and previous pointers, so there would be no differences in the diagram. Note: If there were a spur edge, the two coedges on the edge would be linked to each other at the spur vertex end of the edge. That is, the next pointer of the coedge that ended at the spur vertex would point to the coedge that started at the spur vertex, and the previous pointer of the coedge that started at the spur vertex would point to the coedge that ended at the spur vertex. How would this diagram be different if two of the edges were actually a single, closed edge? (For example, assume the top two edges in the diagram were connected to each other, forming a single edge.) There would be no differences in the diagram. If a coedge is on a face that is completely bounded by a closed edge, then the coedge's next and previous pointers point to itself. Coedge Partner Pointers Four Faces Meeting at a Vertex What would the partner pointers look like for the above example? The diagram at the right shows four faces and edges meeting at a common vertex; however, instead of showing next and previous pointers, this diagram shows the partner pointers. Because there are only two coedges on each edge, each coedge on an edge points to the other coedge on the edge. Notice that two coedges on each edge are pointing in opposite directions. This occurs on all solids and on any sheets in which adjacent faces have consistently directed face normals. As with the previous diagram showing next and previous pointers, there would be no differences in this diagram if any of the edges were spur or prop edges, or if a closed edge were present. Coedge partner pointers provide a means to obtain all the faces surrounding an edge. In fact, because the coedge partner pointer list is an ordered linked list (that is, the coedges are listed in a counter clockwise order about the edge), we know the order of the faces around the edge. If there are no internal or external sheet faces connected to an edge, the directions of the coedges surrounding the edge will alternate. In other words, each coedge will be aligned in the opposite direction as its partner. Why does this always occur? Hint: Think about the directions of the face normals of the faces surrounding the edge. Wire Topology Four Wire Edges Meeting at a Vertex Each wire points to a single coedge in the wire. In fact, the wire can point to any coedge in the wire. Each coedge points to its next and previous coedges in wire. Unlike coedges in solids and sheets, coedges in wires do not have partners. Similar to solids and sheets, the direction of a coedge in a wire may be the same as the direction of the edge, or it may be in the opposite direction of the edge. The next and previous coedge pointers for wires are slightly more complicated than for solids and sheets. The reasons for this are (1) the coedges are not necessarily connected head-to-tail at a vertex, (2) there may be any number of edges meeting at a vertex, and (3) there are no partner pointers. The diagram to the right provides one configuration for a set of four edges/coedges meeting at a vertex. Notice that if a coedge ends at the vertex, its next pointer points to another coedge at the vertex. Alternatively, if a coedge starts at the vertex, its previous pointer points to another coedge at the vertex. All of the coedges starting or ending at a vertex can be obtained by traversing the next and previous coedge pointers about the vertex. The order in which the coedges are obtained by traversing next and previous pointers is not specific. In other words, coedges are not ordered about a wire vertex as they are about an edge connected to a set of faces. If there is only one edge starting or ending at a vertex, that is, the vertex is a spur vertex, then the coedge's next or previous pointer (whichever corresponds to the spur vertex end of the coedge) will point to the coedge. This is also depicted in the diagram. If there is only one edge starting and ending at a vertex, that is, the edge is a closed edge and there are no other edges connected to the vertex, then the coedge's next and previous pointers will point to the coedge. This is similar to what occurs for a closed edge lying in a face. Connecting Solids and Sheets with Wires We have already described how multiple solid regions and/or multiple sheet regions can be connected using non-manifold vertices and/or non-manifold edges. But how can a solid or sheet region be connected to a wire region? A Non-manifold Vertex Connecting a Face to Two Wire Edges The answer is relatively straightforward. Wires can only be attached to a sheet or solid at a vertex. (Why can they not be attached along an edge?) Each wire region is treated as a separation surface at the non-manifold vertex. That is, the non-manifold vertex points to the adjacent edge in the wire as well as an edge on the solid or sheet. If the wire connects to the middle of an existing face, the face will contain a NULL edge (an isolated vertex) and the non-manifold vertex will point to the NULL edge on the face. The figure to the right depicts a face with two wires attached to it at a NULL edge. (Remember, whenever a wire touches a face, we split the wire at the face; therefore, we must have two wires, not one.) What is the topology for this case? The face has two loops: one is a closed edge and one is a NULL edge. The peripheral loop contains a single coedge, whose next and previous coedge pointers point to itself. The other loop also contains a single coedge, whose next and previous coedge pointers point to itself. Each wire contains a single edge. The coedges on each edge have next and previous pointers which point to themselves. So how are the two wires and the face connected? The non-manifold vertex contains pointers to each wire edge and the NULL edge on the face. Additional Comments on Topology In what situations would you expect a coedge's next, previous, or partner pointer to be NULL? The next and previous pointers should never be NULL in a wire. In a solid or sheet, the next and previous pointers may be NULL if the loop is incomplete, implying that the face is only partially bounded. Partner pointers should always be NULL in a wire. Partner pointers are NULL in a solid or sheet if there is no adjacent face at an edge. This situation occurs frequently in sheets; however, because solids are generally closed, it rarely occurs in solids. (It typically means the solid is open or incompletely bounded.) Often when you encounter an incomplete loop or solid, it implies the object is still under construction. In what situations would you expect a coedge's next, previous, or partner pointer to point to the coedge? A coedge's next or previous coedge pointer may frequently point to the coedge. For instance, if an edge is closed, its next and previous pointers point to itself. If an isolated vertex exists in a face, the coedge associated with the NULL edge has next and previous pointers which point to the coedge. In a wire when a spur edge exists, the coedge associated with the edge will have a pointer to itself at the spur vertex. (Depending on whether the coedge starts or ends at the spur vertex will dictate whether it is the previous or next pointer that points to the coedge.) How do you create these various topological configurations? Typically you would use higher level operations to create models. You do not typically create individual topological entities and set their pointers. The preceding discussions are intended to help you understand the various cases that must be handled by your application's algorithms. The table below presents a small subset of the functions by which topological entities are created. Description Operation Name API Function To create a solid body from a set of faces Stitch api_stitch To create a solid body from a closed sheet body Enclose api_enclose_void To create a sheet body from a solid body None api_body_to_2d To create a solid block primitive None api_make_cuboid To create a solid or sheet body by sweeping a face Sweep api_sweep_with_options To create a face from a set of edges Cover Sweep Skin Loft Net api_cover_wire To create an offset of a face Face Offset api_offset_face To create an offset of a body Body Offset api_offset_body To move one or more faces of a body Tweak api_move_faces To create an unbranched wire from a set of edges None api_make_ewire To create an unbranched wire from a set of positions None api_make_wire To create an offset of an unbranched, planar wire Wire Offset api_offset_planar_wire To create a multi-dimensional body To connect a solid body and a sheet body To connect a solid body and a wire body To connect a sheet body and a wire body Non-regularized Unite api_boolean Traversing ACIS Topology There are two fundamentally different ways to traverse the topological data structures in ACIS. One way is to follow the pointers within the various topological classes. This approach uses the member functions of the various topological classes. An example of this would be the EDGE::coedge method which returns a pointer to the first coedge on an edge. The other approach is to use global search functions. These functions traverse the pointers for you and return a list or multiple lists of ENTITYs. An example, if you passed a pointer to a face to the get_edges function, you would obtain a list of the edges bounding the given face. As an example, given an edge on a face how would you find an adjacent edge on the face that shared the start vertex of the edge? You could find the coedge of the edge that lies in the face, determine if the coedge is reversed or in the same direction as the edge, find the next or previous coedge (depending on if the first edge's coedge was reversed or not), then get the edge from the adjacent coedge. That might take you bit of effort to implement in software, but it would be very efficient once you coded it. Alternatively, you could call one function to give you all the edges of the face and another function to give you all the edges of the vertex, and you could compare the edges of the first list with the edges of the second list, and usually there would be only two edges that were contained in both lists: the original edge and the adjacent edge. However, there may be more than two edges on the face that are bounded by the vertex. In that case, you might have to implement a more complex algorithm to find the adjacent edge you were interested in. Hopefully you can see there are advantages to both algorithms. The first algorithm is very efficient a finding one adjacent edge, but would require more effort to find all adjacent edges. The second algorithm would be much less efficient to find one adjacent edge, but if you were looking for all adjacent edges it might be simpler to understand and easier to debug. What Are the Commonly Used ENTITY Methods for Traversing the Topology? The following table contains the most commonly used topological traversal methods. There are three types of methods: those that traverse upward, those that traverse laterally, and those that traverse downward. The methods are in the above type order (that is, first upward traversing methods, then laterally traversing methods, then downward traversing methods.) BODY method Description BODY::owner() Returns NULL, because bodies are the top level topological entity. BODY::lump() Returns a pointer to the first LUMP of this BODY. LUMP method Description LUMP::body() Returns a pointer to the owning BODY, if there is one. LUMP::owner() Returns a pointer to the owning BODY, if there is one. LUMP::next() Returns a pointer to the next LUMP of the owning BODY, if there is one. LUMP::shell() Returns a pointer to the first SHELL of this LUMP, if there is one. SHELL method Description SHELL::lump() Returns a pointer to the owning LUMP, if there is one. SHELL::owner() Returns a pointer to the owning LUMP, if there is one. SHELL::next() Returns a pointer to the next SHELL of the owning LUMP, if there is one. SHELL::face() Returns a pointer to the first FACE in a complete list of all FACEs owned by this SHELL; that is, it pretends no SUBSHELLs exist. SHELL::face_list() Returns a pointer to the first FACE owned directly by this SHELL; that is, it takes SUBSHELLs into account. SHELL::subshell() Returns a pointer to the first SUBSHELL owned directly by this SHELL, if there is one. SHELL::wire() Returns a pointer to the first WIRE in a complete list of all WIREs owned by this SHELL; that is, it pretends no SUBSHELLs exist. SHELL::wire_list() Returns a pointer to the first WIRE owned directly by this SHELL; that is, it takes SUBSHELLs into account. SUBSHELL method Description SUBSHELL::parent() Returns a pointer to the owning SUBSHELL, if there is one. SUBSHELL::owner() Returns a pointer to the owning SUBSHELL, if there is one. SUBSHELL::sibling() Returns a pointer to the next SUBSHELL of the owning SHELL or SUBSHELL, if there is one. SUBSHELL::child() Returns a pointer to the first SUBSHELL owned directly by this SUBSHELL, if there is one. SUBSHELL::face_list() Returns a pointer to the first FACE owned directly by this SUBSHELL; that is, it takes SUBSHELLs into account. SUBSHELL::wire_list() Returns a pointer to the first WIRE owned directly by this SUBSHELL; that is, it takes SUBSHELLs into account. WIRE method Description WIRE::shell() Returns a pointer to the owning SHELL, if it is directly owned by one. WIRE::subshell() Returns a pointer to the owning SUBSHELL, if there is one. WIRE::owner() Returns a pointer to the SHELL or SUBSHELL owning this WIRE, if there is one. WIRE::next() Returns a pointer to the next WIRE in a complete list of all WIRES owned by its SHELL; that is, it pretends no SUBSHELLs exist. WIRE::next_in_list() Returns a pointer to the next WIRE owned directly by its SHELL or SUBSHELL; that is, it takes SUBSHELLs into account. WIRE::coedge() Returns a pointer to a COEDGE in the WIRE. FACE method Description FACE::shell() Returns a pointer to the owning SHELL, regardless of whether or not it is directly owned by a SHELL. FACE::subshell() Returns a pointer to the owning SUBSHELL, if there is one. FACE::owner() Returns a pointer to the SHELL owning this FACE, if there is one; that is, it pretends no SUBSHELLs exist. FACE::next() Returns a pointer to the next FACE in a complete list of all FACEs owned by its SHELL; that is, it pretends no SUBSHELLs exist. FACE::next_in_list() Returns a pointer to the next FACE owned directly by its SHELL or SUBSHELL; that is, it takes SUBSHELLs into account. FACE::loop() Returns a pointer to a LOOP in the FACE, if there is one. LOOP method Description LOOP::face() Returns a pointer to the owning FACE, if there is one. LOOP::owner() Returns a pointer to the owning FACE, if there is one. LOOP::next() Returns a pointer to the next LOOP of the owning FACE, if there is one. LOOP::start() Returns a pointer to a COEDGE of the LOOP. COEDGE method Description COEDGE::loop() Returns a pointer to the owning LOOP, if there is one. COEDGE::wire() Returns a pointer to the owning WIRE, if there is one. COEDGE::owner() Returns a pointer to the owning LOOP or WIRE, if there is one. COEDGE::next() Returns a pointer to the next COEDGE in the LOOP or WIRE. COEDGE::previous() Returns a pointer to the previous COEDGE in the LOOP or WIRE. COEDGE::partner() Returns a pointer to the next COEDGE of the EDGE, if there is one. COEDGE::edge() Returns a pointer to the EDGE of the COEDGE. EDGE method Description EDGE::coedge() Returns a pointer to the owning COEDGE, if there is one. EDGE::owner() Returns a pointer to the owning COEDGE, if there is one. EDGE::end() Returns a pointer to the end VERTEX of this EDGE. EDGE::start() Returns a pointer to the start VERTEX of this EDGE. VERTEX method Description VERTEX::count_edges() Returns the number of EDGEs pointed to by this VERTEX (that is, the number of separation surfaces at this VERTEX.) VERTEX::edge() Returns an owning EDGE, if the VERTEX is manifold. If it is non-manifold, it returns NULL. VERTEX::edge(int) Returns the ith EDGE pointed to by this VERTEX. (Remember a VERTEX points to one EDGE on each separation surface.) VERTEX::owner() Returns a pointer to an owning EDGE. Unless you specifically want to use SUBSHELLS in your algorithms, you probably will never call any of the SUBSHELL methods, SHELL::face_list, SHELL::wire_list, FACE::next_in_list, or WIRE::next_in_list. If we omit these SUBSHELL related methods, and the owner methods which duplicate more specific methods, the most commonly used methods are summarized in the figure below. Most Commonly Used Topological Traversal Methods Example: How Would You Find the BODY Containing a Given Topological ENTITY? All classes defined from the ENTITY class have an is_<entity_class_name>(const ENTITY*) function, which returns whether or not a given ENTITY of is of that type or not. For instance, the is_BODY(const ENTITY*) function returns whether or not an ENTITY is a BODY. Given this function and the above described owner methods, you could do something similar to the following: ENTITY const *owner_ptr = ent; while ( !is_BODY(owner_ptr) ) owner_ptr = owner_ptr->owner(); This will iteratively search up the topological structure until it finds either a BODY or a NULL ENTITY (that is, an ENTITY that did not have an upper topological ENTITY.) This does not work for SUBSHELLs. Top level SUBSHELLs do not contain a pointer to their owning SHELL. Alternatively, you could call get_owner(ENTITY*) or api_get_owner to obtain the top level ENTITY and then use is_BODY(const ENTITY*) to guarantee that the top level ENTITY is indeed a BODY. What Are the Commonly Used Global Functions for Traversing the Topology? There are two sets of functions: Direct Interface functions and API functions. These function scan upward or downward for entities, finding entities containing or contained by the given entity. They do not scan horizontally; that is, they will not find adjacent entities of the same type. As you may notice, there is an API function for each Direct Interface function, and vice versa. How do you choose between the API function and the Direct Interface function? In general, if you are programming at a low level using other direct interface functions, you might as well use the slightly more efficient Direct Interface function. If you are programming at a higher level using only API functions, you should use the API function. The direct interface functions are declared in get_top.hxx. The corresponding API functions are declared in kernapi.hxx. If you look at the lists of Direct Interface functions and the API functions, you will notice that there are no get_bodies or api_get_bodies functions. Why is that? Probably because there is at most one body above any topological entity and it is relatively easy to find the body as we demonstrated in the previous section. In all cases the first argument is the given topological ENTITY and the second argument is the returned list of ENTITYs. For example, if we call get_edges and pass in a pointer to a face as the first argument, the function will return a list containing all the edges of the given face. The last three rows of the table above reflect classes we have not discussed previously: TEDGEs, TCOEDGEs, and TVERTEXs. These are tolerant edges, tolerant coedges, and tolerant vertices. Tolerant entities are discussed in Tolerant Modeling. The ENTITY_LIST class The second argument of each of these functions is a reference to an ENTITY_LIST. The ENTITY_LIST class is defined in lists.hxx. An ENTITY_LIST is a container class. It maintains a list of ENTITYs. The data structures and algorithms associated with the ENTITY_LIST class have been optimized for efficiency. It is not simply a linked list. The table below contains some of the more useful methods of the ENTITY_LIST class. ENTITY_LIST method Description ENTITY_LIST::ENTITY_LIST() Creates an empty ENTITY_LIST. ENTITY_LIST::ENTITY_LIST(ENTITY_LIST const&) The copy constructor. ENTITY_LIST::operator=() The assignment operator. ENTITY_LIST::operator[]() The subscript operator. It returns the entry at a given index, if there is one. If the entry has been deleted (that is, there is only a "tombstone" remaining in the list) the operator returns LIST_ENTRY_DELETED (-1). ENTITY_LIST::add(ENTITY*) Adds an entity to the list, if it is not already in the list. ENTITY_LIST::add(ENTITY_LIST const&) Adds a list of entities to the list, if they are not already in the list. ENTITY_LIST::clear() Removes all the entries from the list. ENTITY_LIST::count() Returns the number of entries in the list, including deleted entities. ENTITY_LIST::init() Initializes an iterator for the next( ) method. ENTITY_LIST::iteration_count() Returns the number of entries in the list, not including deleted entities. ENTITY_LIST::lookup() Returns the index of an entity if it is in the list, or -1 if it is not in the list. ENTITY_LIST::next() Returns the next undeleted entry in the list, or NULL if the end of the list has been reached. ENTITY_LIST::remove(ENTITY const*) Removes an entity from the list. (This does not deallocate any memory, rather it leaves a "tombstone" in the list that will be noticed by count( ) and operator[ ].) As you can probably tell from the above descriptions, there are two simple ways to iterate through an ENTITY_LIST. You can iterate through all the entries in the list using the subscript operator and checking for removed entries, or you can iterate through all the non-deleted entries using the init and next methods. When an attempt is made to add an ENTITY to an ENTITY_LIST, the ENTITY is added only if the ENTITY is not already present in the list. In other words, an ENTITY will be contained at most one time by a list. Its index value will be unique. The ENTITY_LIST class does not know about changes to the model. For instance, if an ENTITY is deleted, the ENTITY_LIST will continue to contain the deleted ENTITY . It is up to the application developer to keep ENTITY_LISTs up-to-date. In addition, an ENTITY_LIST does not participate in history roll back. If you need a list that does participate in history roll back, you should consider the EE_LIST. If you need a list that contains other than ENTITYs, you should consider a VOID_LIST. It contains entries of type (void*). More on ACIS Topology Face Types Previously we mentioned that a face may be a sheet face, or it may bound a solid region, or it may be embedded inside a solid region. How can one determine which type of face one has? Two FACE methods are used to make this determination. FACE::sides returns either SINGLE_SIDED or DOUBLE_SIDED. If a face bounds a solid region it will return SINGLE_SIDED; otherwise, it will return DOUBLE_SIDED. FACE::cont returns either BOTH_INSIDE or BOTH_OUTSIDE. This only applies to DOUBLE_SIDED faces. If the face is a sheet face, with both sides of the face pointing away from the material of the sheet face, then it returns BOTH_OUTSIDE. If the face is embedded inside a solid region, with both sides of the face pointing into the material of the solid, then it returns BOTH_INSIDE. A face cannot be both inside and outside a solid region. The face must be split at the points where it crossed the solid's boundary. Wire Types We also mentioned that a wire may be either outside or inside a volume. How can one determine which type of wire one has?...by using the WIRE::cont method. WIRE::cont returns either ALL_INSIDE or ALL_OUTSIDE. If the wire is embedded inside a solid region, it returns ALL_INSIDE. If it is completely outside a solid region, it returns ALL_OUTSIDE. A wire cannot be both inside and outside a solid region. The wire must be split at the point(s) where it crossed the solid's boundary. Bounding Boxes A very useful tool associated with most topological entities is its bounding box. The bounding box is aligned with the coordinate axes and completely contains the topological entity. A bounding box is not necessarily of minimal size, because it is often much faster to calculate a loose box than a tight box. The easiest way to obtain the bounding boxes of topological entities is to use the overloaded API function api_get_entity_box that takes a SPAboxing_options. The default values for the SPAboxing_options are recommended. Example 1 In the first example, we shall create a solid body and traverse its topology using both the topological ENTITY methods and the direct interface functions. The source code for this example is shown below. C++ Example 1 #include <stdio.h> #include "acis.hxx" #include "api.hxx" #include "kernapi.hxx" #include "cstrapi.hxx" #include "lists.hxx" #include "alltop.hxx" #include "get_top.hxx" // Declaration of the ACIS licensing function. void unlock_spatial_products_<NNN>(); // Declaration of our functions. void do_something(); int my_initialization(); int my_termination(); // The main program... int main (int argc, char** argv) { int ret_val = my_initialization(); if (ret_val) return 1; do_something(); ret_val = my_termination(); if (ret_val) return 1; printf ("Program completed successfully\n\n"); return 0; } void do_something(){ API_BEGIN // Create a block. BODY * my_body; result = api_make_cuboid (10.0, 10.0, 10.0, my_body); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in api_make_cuboid() %d: %s\n", err_no, find_err_mess (err_no)); sys_error (err_no); } // First let's demonstrate the use of the global // topological traversal functions. ENTITY_LIST lump_list; ENTITY_LIST shell_list; ENTITY_LIST wire_list; ENTITY_LIST face_list; ENTITY_LIST edge_list; ENTITY_LIST vertex_list; get_lumps (my_body, lump_list); get_shells (my_body, shell_list); get_wires (my_body, wire_list); get_faces (my_body, face_list); get_edges (my_body, edge_list); get_vertices (my_body, vertex_list); int num_lumps = lump_list.count(); int num_shells = shell_list.count(); int num_wires = wire_list.count(); int num_faces = face_list.count(); int num_edges = edge_list.count(); int num_vertices = vertex_list.count(); printf ("The body has :\n"); printf ("\t %d lumps\n", num_lumps); printf ("\t %d shells\n", num_shells); printf ("\t %d wires\n", num_wires); printf ("\t %d faces\n", num_faces); printf ("\t %d edges\n", num_edges); printf ("\t %d vertices\n", num_vertices); printf ("\n"); // Let's traverse one of the lists using the index operator. // Let's see how many edges each of the faces have. int i; ENTITY_LIST temp_edge_list; for (i = 0; i < num_faces; i++) { FACE * f = (FACE*) face_list[i]; get_edges (f, temp_edge_list); int temp_num_edges = temp_edge_list.count(); printf ("face[%d] has %d edges\n", i, temp_num_edges); temp_edge_list.clear(); } printf ("\n"); // Let's traverse one of the lists using init() and next(). // Let's see how many faces each of the edges have. i = 0; EDGE * e = NULL; edge_list.init(); ENTITY_LIST temp_face_list; while (e = (EDGE*) edge_list.next()) { get_faces (e, temp_face_list); int temp_num_faces = temp_face_list.count(); printf ("edge[%d] has %d faces\n", i++, temp_num_faces); temp_face_list.clear(); } printf ("\n"); // Now let's demonstrate the use of some of // the topological ENTITY class methods. // Let's see how many coedges each of the faces have. for (i = 0; i < num_faces; i++) { FACE * f = (FACE*) face_list[i]; int temp_num_coedges = 0; LOOP * l = f->loop(); while (l) { COEDGE * first_coedge = l->start(); COEDGE * c = first_coedge; do { temp_num_coedges++; c = c->next(); } while (c != first_coedge); l = l->next(); } printf ("face[%d] has %d coedges\n", i, temp_num_coedges); } printf ("\n"); // Let's see how many edges each of the vertices have. // We shall assume that the vertex is a manifold vertex // and that each of the edges bounded by this vertex // is a manifold edge. We shall also assume that each // face of each vertex has at least two edges. (In other // words, there are no closed edges.) These assumptions // make the algorithm much easier to understand. for (i = 0; i < num_vertices; i++) { VERTEX * v = (VERTEX*) vertex_list[i]; // The vertex points to one edge bounded by the vertex. // We must find the other edges by traversing coedges. EDGE * first_edge = v->edge(); // Get the coedge pointed to by the edge. COEDGE * first_coedge = first_edge->coedge(); // If this coedge is directed toward the vertex, // get its partner which is directed away from the vertex. if (first_coedge->end() == v) first_coedge = first_coedge->partner(); // Now we can easily traverse around the edges of the vertex. COEDGE * c = first_coedge; int temp_num_edges = 0; do { // We have an uncounted edge associated with coedge 'c'. temp_num_edges++; // Get c's partner. // It is directed toward the vertex. COEDGE * p = c->partner(); // Get the next coedge. // It is directed away from the vertex. c = p->next(); } while (c != first_coedge); printf ("vertex[%d] has %d edges\n", i, temp_num_edges); } printf ("\n"); // We should delete the BODY before exiting this function. result = api_delent (my_body); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in api_delent() %d: %s\n", err_no, find_err_mess (err_no)); sys_error (err_no); } API_END if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in do_something() %d: %s\n", err_no, find_err_mess (err_no)); } else printf ("do_something() completed successfully!\n\n"); } int my_initialization() { // Start ACIS. outcome result = api_start_modeller(0); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in api_start_modeller() %d: %s\n", err_no, find_err_mess (err_no)); return err_no; } // Call the licensing function to unlock ACIS. unlock_spatial_products_<NNN>(); // Initialize all necessary components. result = api_initialize_constructors(); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in api_initialize_constructors() %d: %s\n", err_no, find_err_mess (err_no)); return err_no; } return 0; } int my_termination() { // Terminate all necessary components. outcome result = api_terminate_constructors(); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in api_terminate_constructors() %d: %s\n", err_no, find_err_mess (err_no)); return err_no; } // Stop ACIS and release any allocated memory. result = api_stop_modeller(); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf ("ERROR in api_stop_modeller() %d: %s\n", err_no, find_err_mess (err_no)); return err_no; } return 0; } The first thing that should be apparent in this example is we added some new header files. We added api.hxx to define the API_BEGIN and API_END macros. We added cstrapi.hxx because the API function to generate a block, api_make_cuboid, is in the constructors components not in the kernel. We added alltop.hxx and get_top.hxx to provide the definitions of the topological entities and the global topological traversal functions. And we added lists.hxx to provide the definition of the ENTITY_LIST class. The next thing to notice is that instead of initializing and terminating the kernel component, we initialized and terminated the constructors component. This is the only difference in our initialization and termination functions. The reason for this is we used an API function in the constructors component to create a block. So, this leaves us with the do_something function. You will notice that the majority of the function is contained within the API_BEGIN and API_END statements. We will describe these macros in much more depth in Tutorials (Exception Handling). For now let's just say that they provide us with exception handling capabilities and they cause all of the model changes to be recorded using the ACIS history mechanism. Any errors that are propagated within the API_BEGIN/API_END block will be caught by the API_END statement. (We are intentionally being vague about the mechanism for propagating errors, because the implementation of these macros may vary from release to release and depends upon on platforms and compilers.) Within the API_BEGIN/API_END block we use the sys_error function to propagate an error. After sys_error is called, the processing of this function resumes after the API_END statement. So, what happens between the API_BEGIN and API_END statements? The function begins by constructing a block with dimensions 10x10x10. If the block construction fails, we print an error message and effectively abort the algorithm. (You can observe what happens in the event of an error by passing in an invalid value for one of the block's dimensions. For instance, modify one of the dimensions of the block to be -1.0 and rerun the test application. What happens?) After constructing our block we then use the global topological traversal functions to obtain lists of the lumps, shells, wires, faces, edges, and vertices of the block. We report the number of each of these classes of objects by determining the size of each of the lists. Next, we demonstrate the traversal of the list of faces using the ENTITY_LIST index operator. For each face in the list, we determine how many edges it has by obtaining a list of edges and reporting its size. Following this, we demonstrate the traversal of the list of edges using the init and next methods. For each edge in the list, we determine how many faces it has by obtaining a list of faces and reporting its size. Traversing Coedge "partner" and "next" Pointers This is followed by counting the coedges on each face. This is done by traversing each face's singly-linked list of loops and each loop's doubly-linked list of coedges. (We simply follow the coedges' "next" pointers until we return to the coedge we started with.) The final traversal demonstration counts the number of edges connected to each vertex of the block. This traversal algorithm makes several assumptions. We assume (1) there are no non-manifold edges, (2) there are no non-manifold vertices, and (3) there are no closed edges. These are all true for the block which we are traversing. The topology at each vertex looks similar to what is shown in the diagram to the right. As you can see from this diagram we can traverse the coedges by simply following "partner" and "next" pointers. Given that we can traverse the coedges around each vertex, and we can easily obtain the edge from each coedge, we can thereby gather all the edges around each vertex. Before exiting the function we "delete" the block. We will discuss memory allocations and de-allocations more thoroughly in Tutorials (Memory Management). Example 2 In the second example, we shall restore a wire body from a file and traverse its topology to count the number of edges, coedges, and vertices. This example also demonstrates how to generate a debug file containing a dump of the ACIS data structure. C++ Example 2 #include <stdio.h> #include "acis.hxx" #include "api.hxx" #include "api.err" #include "kernapi.hxx" #include "lists.hxx" #include "alltop.hxx" #include "get_top.hxx" #include "debug.hxx" // Declaration of the ACIS licensing function. void unlock_spatial_products_<NNN>(); // Declaration of our functions. void do_something(); int my_initialization(); int my_termination(); // The main program... int main (int argc, char** argv) { int ret_val = my_initialization(); if (ret_val) return 1; do_something(); ret_val = my_termination(); if (ret_val) return 1; printf("Program completed successfully\n\n"); return 0; } void do_something(){ API_BEGIN // Retrieve a wire body from a SAT file. // Open the SAT file. FILE * fp = fopen("wire.sat", "r"); if (fp == NULL) { printf("ERROR opening wire.sat\n"); sys_error(API_FAILED); } // Restore all the entities from the SAT file. ENTITY_LIST ents; result = api_restore_entity_list(fp, TRUE, ents); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in api_restore_entity_list() %d: %s\n", err_no, find_err_mess(err_no)); sys_error(err_no); } // Close the SAT file. fclose(fp); // We expect the first ENTITY to be a BODY. BODY * my_body = NULL; if (is_BODY(ents[0])) my_body = (BODY*) ents[0]; else { printf("The SAT file does not contain a BODY\n"); sys_error(API_FAILED); } // Verify the body contains a single wire body. WIRE * my_wire = NULL; if (my_body->lump() && my_body->lump()->next() == NULL && my_body->lump()->shell() && my_body->lump()->shell()->next() == NULL && my_body->lump()->shell()->wire() && my_body->lump()->shell()->face() == NULL) my_wire = my_body->lump()->shell()->wire(); else { printf("The SAT file does not contain a wire body\n"); sys_error(API_FAILED); } // Let's obtain lists of all the coedges, edges, and vertices // contained in the wire. We could do this using the global // topological traversal functions, but let's show how this // can be done using ENTITY methods. ENTITY_LIST coedge_list; ENTITY_LIST edge_list; ENTITY_LIST vertex_list; // Plant the start coedge in the list. coedge_list.add(my_wire->coedge()); // For each coedge in list, plant its next and previous coedges. // Continue planting coedges in the list until all have been added. for (int i = 0; ; ++i) { COEDGE *this_coedge = (COEDGE *)(coedge_list[i]); if (this_coedge == NULL) break; coedge_list.add(this_coedge->next()); coedge_list.add(this_coedge->previous()); } // Given the list of coedges, it is simple to obtain // the lists of edges and vertices. coedge_list.init(); COEDGE * c; while (c = (COEDGE*)coedge_list.next()) { edge_list.add(c->edge()); vertex_list.add(c->start()); vertex_list.add(c->end()); } int num_coedges = coedge_list.count(); int num_edges = edge_list.count(); int num_vertices = vertex_list.count(); printf("The wire body has :\n"); printf("\t %d coedges\n", num_coedges); printf("\t %d edges\n", num_edges); printf("\t %d vertices\n", num_vertices); printf("\n"); // Finally, debug the wire body to a file. fp = fopen("wire_body.dbg", "w"); if (fp == NULL) { printf("ERROR opening wire_body.dbg\n"); sys_error(API_FAILED); } debug_entity(my_body, fp); fclose(fp); // We should delete the BODY before exiting this function. result = api_delent(my_body); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in api_delent() %d: %s\n", err_no, find_err_mess(err_no)); sys_error(err_no); } API_END if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in do_something() %d: %s\n", err_no, find_err_mess(err_no)); } else printf("do_something() completed successfully!\n\n"); } int my_initialization() { // Start ACIS. outcome result = api_start_modeller(0); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in api_start_modeller() %d: %s\n", err_no, find_err_mess(err_no)); return err_no; } // Call the licensing function to unlock ACIS. unlock_spatial_products_<NNN>(); // Initialize all necessary components. result = api_initialize_kernel(); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in api_initialize_kernel() %d: %s\n", err_no, find_err_mess(err_no)); return err_no; } return 0; } int my_termination() { // Terminate all necessary components. outcome result = api_terminate_kernel(); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in api_terminate_kernel() %d: %s\n", err_no, find_err_mess(err_no)); return err_no; } // Stop ACIS and release any allocated memory. result = api_stop_modeller(); if (!result.ok()) { err_mess_type err_no = result.error_number(); printf("ERROR in api_stop_modeller() %d: %s\n", err_no, find_err_mess(err_no)); return err_no; } return 0; } Comparing the header files of this example with the first example, you should notice that cstrapi.hxx has been removed (because we are no longer using the constructor component to construct the block) and we have added to new header files: api.err and debug.hxx. The file api.err was added so that we could generate an error message in the event we could not open the file containing the wire body. When generating the error, we use the symbol API_FAILED, which produces a very generic error message about the function failing. In this example, we again initialized and terminated the kernel component. The function begins by attempting to open the file wire.sat. We could have queried for the file name or passed it in as an argument to the main program. However, to keep the logic of the function as simple as possible, we simply hard coded the file name. (This implies that you need to name your SAT file wire.sat.) Refer to Tutorial: SAT File Example for a SAT file containing a wire body. Where you should put this file to be read by the application depends on your operating system and how you execute the example program. But, often it is in the same directory as your executable. If you get an error message about not being able to open the file, it probably means it is improperly named or in the wrong location. After successfully opening the SAT file containing the wire body, the program reads the contents of the SAT file using the API function: api_restore_entity_list. This function returns an ENTITY_LIST containing all of the top level ENTITIES. (For instance, if the file contained three bodies and all their lower topology, the ENTITY_LIST would contain the three bodies.) After opening and reading a SAT file you should always close the file. (In production software, you would probably want to close the file before processing any errors returning from the API function. However, because we are simply exiting in the event of an error and wanting to keep the logic of this example as simple as possible, we processed the error immediately after returning from the API function.) After reading the file, which hopefully contains a single wire body, we check to see if the first entity in the file was indeed a wire body, and if it was not, we issue an error. Our example program counts the number of edges, coedges, and vertices in the wire body. There are many ways to do this. One way would be to simply use the global functions to traverse the topology. We have instead used a seeded search algorithm. We create an ENTITY_LIST and seed it with a known coedge, obtained from the wire. For every coedge in the list, we add its next, previous, and partner coedges. In this way, we traverse all the coedges in the wire, because all coedges can be found by following next, previous, and partner pointers from any given coedge. Would this algorithm work for finding all the coedges on a solid or sheet body? Give this some thought before reading the answer... Answer: It would not. Can you give four reasons why this would not work? (Multiple lumps. Multiple shells. Multiple loops. Non-manifold vertices. Why would each of these answers prevent the algorithm from working?) Can you modify this algorithm to generate a list of all the coedges on a solid or sheet body? Given the list of coedges, it is relatively straightforward to generate lists of edges and vertices, because the edge and vertices associated with each coedge can be obtained directly. The last thing the function does is it creates a debug file containing a dump of the ACIS data structure. Refer to Tutorial: Debug File for an example debug file for the wire body in the sample SAT file. Can you create a diagram depicting the vertices, edges, and coedges, including the directions of the edges and coedges, and the coedges' next and previous pointers? Hint: It is usually easiest to start such a diagram by first locating the vertices. Each vertex has an underlying position. Then each edge can be drawn from its start to its end vertex. Then the coedge of each edge can be drawn, depending on whether it is FORWARD or REVERSED with respect to the edge. Finally add the next and previous pointers of the coedges. You only need to look at the geometry of edges (that is, the curves underlying the edges) if there are multiple edges between two vertices or if there are closed edges. When examining the relationship between edges and their underlying curves, you should realize that an edge, like a coedge, can be either FORWARD or REVERSED, which is with respect to its underlying curve. How to Learn More about ACIS Topology? The best way to understand ACIS topology is to generate ACIS models and examine their topological structures. Nothing can take the place of experience. Use the Scheme AIDE to generate simple models possessing the topological structures you are interested in, debug the data structure into text file, and create diagrams similar to those presented in this tutorial. As an example, the Scheme script to generate the wire.sat file for Example 2 is provided at Tutorial: Scheme Script. If you are not familiar with the Scheme AIDE, the Scheme Reference Guide provides a description of the language and Spatial's geometric modeling extensions. Tutorial 4: Math Classes In this tutorial we shall introduce you to some of the ACIS classes that represent generalized mathematical concepts, such as positions, vectors, and bounding boxes. Some of these classes function in the 3-dimensional model space, some of these classes function in the 2-dimensional parametric space of a surface, and some function in both. Our intent is to familiarize you with some of the most frequently used classes, their member functions and operators, and some of the global functions that operate upon these classes. We have included links to the documentation for each of these classes. Take a few moments to scan through the documentation on the functions and operators for each of the classes to get a sense of capabilities of each of the classes. SPAposition A SPAposition is a point (that is, a position vector) in a 3D Cartesian space. SPApositions are defined in position.hxx. SPAvector A SPAvector is a displacement vector in a 3D Cartesian space. SPAvectors are defined in vector.hxx. SPAunit_vector A SPAunit_vector is a direction in 3D Cartesian space that has unit length. It is derived from the SPAvector class; therefore, it inherits all the functionality of SPAvectors. There are very few operations that are unique to unit vectors, although several functions have been overloaded for unit vectors to make them more efficient. SPAunit_vectors are defined in unitvec.hxx. SPAmatrix A SPAmatrix is a 3x3 Euclidean affine transformation. It is not a tensor. SPAmatrixs are defined in matrix.hxx. Note: Application developers will generally use an instance of the SPAtransf class rather than an instance of the SPAmatrix class. The SPAmatrix class is described here so you will be aware it exists inside a transformation. SPAtransf A SPAtransf is a general transformation for a 3D vector. It is in effect a 4 x 3 matrix to multiply a homogeneous vector, but is stored specially for efficiency. Transformations are applied to 3D positions and vectors. SPAtransfs are defined in transf.hxx. SPAparameter A SPAparameter is a curve parameter value. It is a essentially a double, but it has been declared as a class for consistency. SPAparameters participate in virtually all of the arithmetic operations one would expect for a double. SPAparameters are defined in param.hxx. SPApar_pos A SPApar_pos is a position in the parameter-space of a surface. It defines a (u, v) parameter-space coordinate that, when evaluated on a surface, produces a 3D object space coordinate. SPApar_poses are defined in param.hxx. SPApar_vec A SPApar_vec is a vector (du, dv) in the parameter-space of a surface. SPApar_vecs are defined in param.hxx. SPApar_dir A SPApar_dir is a direction vector (du, dv) in the parameter-space of a surface. (This is the parameter-space analogy to a SPAunit_vector.) It is derived from the SPApar_vec class; therefore, it inherits all the functionality of SPApar_vecs. There are very few operations that are unique to SPApar_dir, although several functions are overloaded for SPApar_dirs to make them more efficient. SPApar_dirs are defined in param.hxx. SPAinterval A SPAinterval is an interval on the real line. A SPAinterval may be bounded or unbounded at either end, allowing a SPAinterval to represent a finite, infinite, or semi-infinite interval. SPAintervals are defined in interval.hxx. Note: We should probably explain what it means to negate an interval. If we start with the bounded interval (1, 2) and negate it, we get the interval (-2, -1). This implies that not only are the lower and upper bounds negated, but they are also swapped. The lower and upper bounds of an interval must be in a non-decreasing order. SPAbox A SPAbox represents a bounding box. It is implemented as an axis-aligned rectangular box, represented by three SPAintervals. Each interval may be bounded or unbounded at either end, allowing a SPAbox to represent a finite, infinite, or semi-infinite bounding box. SPAboxs are defined in box.hxx. Bounding boxes are computed for topological ENTITYs only when needed. That is, their computation is lazy, only upon demand. If an ENTITY is changed, any existing bounding box might be invalid so any such box is deleted; however, once a box is computed, it is saved for subsequent reuse. Boxes are logged for roll back purposes. After a roll back, boxes are set to their previous state. Bounding boxes are also saved in disk files. Note: Bounding boxes are generally obtained by calling one of the get_box functions described in Tutorials (Model Topology) rather than by creating them using a SPAbox constructor. Important: The default behavior of ACIS is not to calculate tight bounding boxes. The bounding box for an ENTITY may be significantly larger than the ENTITY. SPApar_box A SPApar_box is a 2D bounding box in the parameter-space of a surface. (This is the parameter-space analogy to a SPAbox.) It is implemented as an axis-aligned rectangular box, represented by two SPAintervals. SPApar_boxes are defined in param.hxx. Note: Parameter-space boxes are generally obtained from a surface or a face by calling either surface::param_range or sg_get_face_par_box rather than by creating them using a constructor. Laws ACIS contains an extensive set of capabilities to symbolically represent and solve mathematical functions (equations) using laws. Laws are beyond the scope of this tutorial - and are probably inappropriate for a new ACIS user - but you should be aware of their existence. For more information on laws, refer to Laws. Math-related Macros A logical is an integer with two possible values, TRUE or FALSE. logical, TRUE, and FALSE are #define'd in logical.h. In addition, M_PI and M_PI_2 are defined in base.hxx (which is included by acis.hxx) for those platforms that do not have these symbols defined for pi and pi/2. Tutorial 5: Geometry This tutorial provides a brief overview of ACIS geometry, and demonstrates some ways in which geometry can be constructed and queried. Introduction ACIS geometry can be categorized using a variety of criteria. Perhaps the first categorization you should be aware of is the difference between persistent and non-persistent objects. When we talked about topology in Tutorial 3 every topology class was persistent. All topological classes are derived from ENTITY and, therefore, all topological changes are recorded by the ACIS history mechanism. This is not true with geometry classes. Some geometry classes are derived from ENTITY and some are not. To help you distinguish between persistent and non-persistent geometry classes the names of classes that are derived from ENTITY are in UPPERCASE and the names of the ones that are not are in lowercase. (There is an exception to this rule, but in general this rule will help you understand which is which.) Objects that become part of the model are derived from ENTITY. Classes that are not derived from ENTITY are used as part of the definition of persistent geometry classes, and they may be used as construction geometry. Instances of these classes may be created on the stack or on the heap. Before we get too far into this rather abstract discussion, let's describe what the classes are that we are talking about. ACIS geometry falls into four categories: • points that exist in a 3-dimensional space, • curves that exist in a 3-dimensional space, • surfaces that exist in a 3-dimensional space, and • curves that exist in the 2-dimensional space of a parametrically defined surface. Classes of these four types of geometry are shown in the table below. Persistent Class Non-Persistent Class Description APOINT SPAposition A 3-dimensional point CURVE curve A 3-dimensional curve SURFACE surface A 3-dimensional surface PCURVE pcurve A 2-dimensional (parameter space) curve We should mention that CURVE, curve, SURFACE, and surface are abstract base classes. You will always create instances of classes derived from these classes. The APOINT, SPAposition, PCURVE, and pcurve classes are not base classes and, therefore, do not have classes derived from them. (The rather unusual names for the APOINT and SPAposition classes were chosen so that they would not conflict with classes in other third-party libraries. They were originally called POINT and position, but their names were changed several years ago.) Points Main article: Point The most basic of geometrical concepts is a point in space. A point in 3-dimensional space is represented in ACIS by a SPAposition. A SPAposition contains three values representing the x, y, and z coordinates of a point in a 3-dimensional Cartesian coordinate system. In the persistent ACIS model an APOINT is the geometry underlying a VERTEX. Each VERTEX contains a pointer to an APOINT. The geometric definition of an APOINT is stored in a SPAposition. For completeness, we should mention that ACIS also represents points in 1- and 2-dimensional spaces. The classes corresponding to 1- and 2-dimensional points are SPAparameter and SPApar_pos. (Yes, these were originally called parameter and par_pos.) There are no persistent classes that represent 1- and 2-dimensional points because they are not needed by the persistent ACIS model structure. Curves Main article: Curve The concept of a curve is implemented in ACIS in the curve and CURVE classes. Classes derived from curve are not persistent; those derived from CURVE are persistent. The geometry underlying EDGEs must be persistent, so CURVEs are used in the model structure. Each EDGE contains a pointer to a CURVE. There are several types of CURVEs—and for each CURVE type there is an associated (lower-case) curve type—so let's briefly describe curves conceptually, before we get into the class distinctions. Each curve maps a single parameter value t into a 3-dimensional point (x,y,z). Each curve has a parameter range that restricts the values the parameter may have, although in some cases a parameter range may be infinite, which means the parameter may take on any value. Curves may be open, closed, or periodic. Periodic means two things in ACIS: • If a curve is periodic with a period p, then the position corresponding to a parameter value t is the same position that corresponds to the parameter t + np, where n is any integer. In other words, if you evaluate a periodic curve at t + np, you will get the same position for any integer value of n. • The curve is at least G1-continuous across the seam. The seam for a periodic curve is the point corresponding to the start and end of the principal parameter range. The principal parameter range for a periodic curve is the range into which inverse evaluations are mapped. [An evaluation of a curve maps a parameter value t into a 3-dimensional point (x,y,z). An inverse evaluation of a point on a curve maps a 3-dimensional point (x,y,z) into a parameter value t.] For example, the principal parameter range of an ellipse is [ − π,π), so at the point corresponding to a parameter value of π (or − π) the curve must be at least G1-continuous. ACIS defines three types of analytic curves: straight lines, ellipses, and helices. It defines a non-uniform rational B-spline curve. It defines many types of procedurally defined curves (for example, a surface-surface intersection curve). And it defines a composite curve, which is an aggregation of other curves. The following table describes the specific classes used to represent these types of curves. Persistent Class Non-Persistent Class Description STRAIGHT straight A straight line ELLIPSE ellipse An elliptical curve HELIX helix A helical curve INTCURVE intcurve A NURBS curve, a procedural curve, or a composite curve curves (lower-case classes) The curve class defines many virtual functions, including the following: • determining the parameter range of a curve • determining if a curve is open, closed, or periodic • determining the position, tangent, or curvature at a specific parameter value • determining the parameter value corresponding to a position on the curve • determining the foot of the perpendicular line from a given point to the curve • determining the closest point on a curve to a given point • determining if a given point is within a given tolerance of the curve • determining the length of the curve between two parameter values. The specific curve types are described below. A straight represents a straight line. The form of a straight is always open. The parameter range of a straight may be (and generally is) limited to a subset of the real-number line. An ellipse represents a full or partial elliptical curve. An ellipse may represent a full or partial circle. The period of a full ellipse is . The parameter range of an ellipse depends on whether or not it is a full ellipse. If it is a full ellipse, the parameter range is [ − π,π). If it is a partial ellipse, the parameter range is the real interval to which the ellipse has been limited. The form of an ellipse may be open, closed, or periodic. (If an ellipse is limited to a parameter range whose length is , the ellipse is closed, but it is not periodic.) A helix represents a general (possibly tapered) helical curve. Special cases of helices include non-tapered curves and planar curves (spirals). The parameter range of a helix is part of its definition. The form of a helix is always open. An intcurve represents an interpolated curve defined over a given parameter range. The form of an intcurve may be open, closed, or periodic. An intcurve is really a wrapper around an int_cur. This makes it much more efficient to copy and manipulate intcurves. The int_cur class is an abstract base class. There are many classes derived from the int_cur class, which demonstrates the flexibility of this class. Some of these are enumerated below. Note: Clients should always use the curve and intcurve interfaces, rather than the int_cur interface. int_cur class Description exact_int_cur A NURBS curve par_int_cur">par_int_cur The 3-dimensional image of a parameter space curve int_int_cur The intersection of two surfaces law_int_cur A curve defined by a user-defined function off_int_cur The intersection of two surfaces offset from two given surfaces offset_int_cur The offset of another curve off_surf_int_cur The offset of a curve lying on a surface along the surface normal para_silh_int_cur A parallel-view silhouette curve persp_silh_int_cur A perspective-view silhouette curve proj_int_cur The perpendicular projection of a curve onto a surface spring_int_cur A spring curve, which is a curve along the edge of a blend surface Another intcurve-related class that you should be aware of is the bs3_curve class. A bs3_curve is a 3-dimensional B-spline curve. (It is actually a NURBS curve.) All int_cur classes contain a bs3_curve. The bs3_curve is generally a B-spline approximation to the procedurally defined int_cur; however, in the case of an exact_int_cur the bs3_curve is considered to be exact, not an approximation; therefore, an exact_int_cur represents a NURBS curve. In addition to a bs3_curve, each int_cur may contain pointers to two surfaces and two bs2_curves. (A bs2_curve is identical to a b3_curve, except that it is defined in two dimensions, not three.) The two surfaces may be used by the procedural definition of the int_cur. Each of the two bs2_curves is (usually) an approximation to the image of the curve projected into the parameter space of the corresponding surface. The exception to this occurs with the par_int_cur class. In the case of a par_int_cur, the bs2_curve is considered to be exact. A par_int_cur is the 3-dimensional curve defined by the 2-dimensional bs2_curve and its associated surface. This discussion of int_curs, bs3_curves, bs2_curves, etc., is intended to familiarize you with some of the terminology and concepts used in ACIS. Most likely, you will never create an int_cur, bs3_curve, or bs2_curve directly—they will be created for you as a result of using higher-level functions that create curves, CURVEs, or EDGEs. For instance, you can create an EDGE based upon a NURBS curve using api_mk_ed_int_ctrlpts. In fact, curves are often created by much higher level operations such as sweeps, blends, or Booleans. Similarly, you most likely will never directly manipulate int_curs, bs3_curves, or bs2_curves. (For instance, directly deleting an int_cur is dangerous. Each int_cur contains a use count reflecting how many intcurves reference it. When all of the intcurves referencing an int_cur have been deleted, the int_cur deletes itself.) CURVEs (upper-case classes) CURVEs are used by the ACIS model structure. They are the geometry underlying EDGEs. There are four types of CURVEs: STRAIGHT, ELLIPSE, HELIX, and INTCURVE. Each UPPER-CASE geometry class contains an instance of its corresponding lower-case geometry class. (In other words, a STRAIGHT contains a straight; an ELLIPSE contains an ellipse; a HELIX contains a helix; and an INTCURVE contains a intcurve.) The CURVE classes also inherit all of the member functions of the ENTITY class, so they can be saved and restored, rolled backward and forward (that is, undone and redone), debugged, and copied, for example. Surfaces Main article: Surface Because the concept and implementation of surfaces is so similar to the concept and implementation of curves, the following discussion of surfaces is very similar to the discussion of curves. The concept of a surface is implemented in ACIS in the surface and SURFACE classes. Classes derived from surface are not persistent; those derived from SURFACE are persistent. The geometry underlying FACEs must be persistent, so SURFACEs are used in the model structure. Each FACE contains a pointer to a SURFACE. There are several types of SURFACEs—and for each SURFACE type there is an associated (lower-case) surface type—so let's briefly describe surfaces conceptually, before we get into the class distinctions. Each surface maps a pair of parameter values (u,v) into a 3-dimensional point (x,y,z). Each surface contains a pair of parameter ranges that restrict the values of the parameters, although in some cases a parameter range may be infinite, which means that the parameter may take on any value. Surfaces may be open, closed, or periodic in either parametric direction. Periodic means two things in ACIS: 1. If a surface is u-periodic with a period p, then the position corresponding to a parameter value u is the same position that corresponds to the parameter u + np, where n is any integer. In other words, if you evaluate a surface that is periodic in a parametric direction at the parameter (u + np,v), you will get the same position for any integer value of n. A v-periodic surface is similarly described. 2. The surface is at least G1-continuous across the seam. The seam for a periodic surface is the curve corresponding to the start and end of the principal parameter range. The principal parameter range for a periodic surface is the range into which inverse evaluations are mapped. [An evaluation of a surface maps a pair of parameter values (u,v) into a 3-dimensional point (x,y,z). An inverse evaluation of a point on a surface maps a 3-dimensional point (x,y,z) into a pair of parameter values (u,v).] For example, a cone is v-periodic. The principal parameter range of a cone in the v direction is [ − π,π), so at the point corresponding to a v parameter value of π (or − π) the surface must be at least G1-continuous in the v direction. ACIS defines four types of analytic surfaces: planes, cones, spheres, and tori. It defines a non-uniform rational B-spline surface and many types of procedurally defined surfaces (for example, a surface of revolution). The following table describes the specific classes used to create these types of surfaces. Persistent Class Non-Persistent Class Description PLANE plane A planar surface CONE cone A conical surface SPHERE sphere A spherical surface TORUS torus A toroidal surface SPLINE spline A NURBS surface, or a procedural surface surfaces (lower-case classes) The surface class defines many virtual functions, including the following: • determining the parameter range of the surface, • determining if a surface is open, closed, or periodic in either parametric direction, • determining the position, normal, or curvature at a specific parametric location, • determining the parameter location corresponding to a position on the surface, • determining the foot of the perpendicular line from a given point to the surface, • determining if a given point is within a given tolerance of the surface. The specific surface types are described below. A plane represents a portion of a planar surface. The form of a plane is always open in both directions. The parameter range of a plane may be (and generally is) limited to a subset of the real-number line in both the u and v directions. A cone represents a portion of an elliptical cone. A cone is frequently used to represent a cylindrical surface (or more precisely, a right circular cylinder). A cone may be open, closed, or periodic in the v-direction. A cone is open in the u-direction. If a cone is periodic in the v-direction, it has a period of and its principal parameter range is [ − π,π). In the u-direction a cone will not extend beyond its apex. If a cone extends to its apex, it is singular in u at the apex. A sphere represents a portion of a spherical surface. A sphere may be open, closed, or periodic in the v-direction. A sphere is open in the u-direction, with a maximum parameter range in the u-direction of [ − π / 2,π / 2]. If a sphere is periodic in the v-direction, it has a period of and its principal parameter range is [ − π,π). If a sphere extends to either of its poles, it is singular in u at that pole. A torus represents a portion of a toroidal surface. A torus may be a doughnut, apple, lemon, or vortex torus. A non-degenerate torus (a doughnut torus) may be open, closed, or periodic in either parametric direction. A degenerate torus (an apple, lemon, or vortex torus) is not periodic in the u-direction, but may be in the v-direction. If a torus is periodic in the u-direction, it has a period of and a parameter range of [ − π,π). If a torus is periodic in the v-direction, it has a period of and a parameter range of [ − π,π). A spline represents a parametric surface, with a specific parameter range. The form of a spline may be open, closed, or periodic in either direction. A spline is really a wrapper around a spl_sur. This makes it much more efficient to copy and manipulate splines. The spl_sur class is an abstract base class. There are many classes derived from the spl_sur class, which demonstrates the flexibility of this class. Some of these are enumerated below. Note: Clients should always use the surface and spline interfaces, rather than the spl_sur interface. spl_sur class Description exact_spl_sur A NURBS surface blend_spl_sur A blend surface ruled_spl_sur A ruled surface sum_spl_sur A sum surface law_spl_sur A surface defined by a user-defined function off_spl_sur The offset of another surface rot_spl_sur A surface of revolution skin_spl_sur A skinned surface net_spl_sur A net surface sweep_spl_sur A swept surface Another spline-related class that you should be aware of is the bs3_surface class. A bs3_surface is a 3-dimensional B-spline surface. (It is actually a NURBS surface.) All spl_sur classes contain a bs3_surface. The bs3_surface is generally a B-spline approximation to the procedurally defined spl_sur; however, in the case of an exact_spl_sur the bs3_surface is considered to be exact, not an approximation; therefore, an exact_spl_sur represents a NURBS surface. This discussion of spl_surs and bs3_surfaces is intended to familiarize you with some of the terminology and concepts used in ACIS. Most likely, you will never create a spl_sur or bs3_surface directly—they will be created for you as a result of using higher-level functions that create surfaces, SURFACEs, or FACEs. For instance, you can create a FACE based upon a NURBS surface using api_mk_fa_spl_ctrlpts. In fact, surfaces are often created by much higher level operations such as offsetting, sweeping, or blending. Similarly, you most likely will never directly manipulate spl_surs or bs3_surfaces. (For instance, directly deleting a spl_sur is dangerous. Each spl_sur contains a use count reflecting how many splines reference it. When all of the splines referencing a spl_sur have been deleted, the spl_sur deletes itself.) SURFACEs (upper-case classes) SURFACEs are used by the ACIS model structure. They are the geometry underlying FACEs. There are five types of SURFACEs: PLANE, CONE, SPHERE, TORUS, and SPLINE. Each UPPER-CASE geometry class contains an instance of its corresponding lower-case geometry class. (In other words, a PLANE contains a plane; a CONE contains a cone; a SPHERE contains a sphere; a TORUS contains a torus; and a SPLINE contains a spline.) The SURFACE classes also inherit all of the member functions of the ENTITY class, so they can be saved and restored, rolled backward and forward (that is, undone and redone), debugged, copied, etc. Parameter-Space Curves (Pcurves) Main article: Parameter Space Curve If an EDGE lies on a FACE, ACIS may contain a representation of the CURVE underlying the EDGE in the parameter space of the SURFACE underlying the FACE. The only cases in which ACIS requires a parameter-space representation of the CURVE are: • if the SURFACE is a SPLINE, or • if the EDGE is a tolerant EDGE. (For details, see the technical article Tolerant Modeling.) For all other cases, a parameter-space representation of a CURVE is optional. That is, for all other cases you may construct one for use by your application, but it most likely will not be used by ACIS algorithms. With the exception of par_int_cur-based curves (and tolerant edges), parameter-space curves are considered to be a secondary representation, generated from the curve and the surface. That is, the parameter-space curves can be deleted and reconstructed using the curve and surface information. The non-persistent class representing parameter-space curves is pcurve, with PCURVE the corresponding persistent class. pcurve (lower-case class) A pcurve is somewhat similar to an intcurve, in the sense that a pcurve is really just a wrapper around a par_cur. This makes it much more efficient to copy and manipulate pcurves. The par_cur class is an abstract base class. There are three classes derived from the par_cur class. These are enumerated below. par_cur class Description exp_par_cur An explicit parameter-space curve (defined by a surface and a bs2_curve) imp_par_cur An implicit parameter-space curve (defined by one of the surfaces and bs2_curves underlying an intcurve) law_par_cur A law-based parameter-space curve (defined by a surface and a 2-dimensional law) We have already mentioned the bs2_curve class. A bs2_curve is a 2-dimensional B-spline curve. (It is actually a NURBS curve.) A bs2_curve underlying a par_cur is an approximation to the image of a curve projected into the parameter space of a surface. Because the evaluation of par_curs is based upon the evaluation of their underlying bs2_curves, the accuracy of par_cur (and, therefore, pcurve) evaluations depends upon the accuracy to which the bs2_curves are generated. In other words, pcurves are not procedural curves. This discussion of par_curs and bs2_curves is intended to familiarize you with some of the terminology and concepts used in ACIS. Most likely, you will never create a par_cur or bs2_curve directly. In fact, most applications do not require direct access to either pcurves or PCURVEs. They will be created and maintained for you as a result of your application's use of higher-level functions. PCURVE (upper-case class) PCURVEs are the geometry underlying COEDGEs, but not all COEDGEs possess PCURVEs. A PCURVE is required only if a COEDGE lies on a SPLINE surface or when the COEDGE is tolerant; otherwise, the PCURVE is optional. Unlike with CURVEs and SURFACEs, there are no classes derived from the PCURVE class. There is only one PCURVE class. However, there are two fundamentally different types of PCURVEs. A PCURVE may have a private definition, meaning it contains a pcurve, or it may use a surface and bs2_curve underlying an intcurve. This flexibility (using the data underlying an intcurve) allows models to be constructed with fewer explicit pcurves. Pcurve-related Functions Some of the most commonly used functions to construct, destruct, and query PCURVEs are described below. To create or remove PCURVEs: void sg_add_pcurve_to_coedge(COEDGE*, ...) void sg_add_pcurves_to_entity(ENTITY*, ...) void sg_rm_pcurves_from_entity(ENTITY*, ...)  Important: Removing necessary PCURVEs with sg_rm_pcurves_from_entity can corrupt your model. Use this function with extreme caution. To obtain the PCURVE pointer from a COEDGE: PCURVE* COEDGE::geometry() const  To obtain a copy of a private pcurve from a PCURVE, or to construct a pcurve from a PCURVE without a private pcurve: pcurve PCURVE::equation() const  Note: PCURVE::equation() returns a pcurve by value, not by reference. To obtain parameter space bounding boxes for a pcurve or FACE: SPApar_box pcurve::bound() const void sg_get_face_par_box(FACE*, SPApar_box&)  We should mention that adding the parameter-space boxes of all the PCURVEs on a FACE that lies on a periodic surface or a surface with singularities may not give you the same result as calling sg_get_face_par_box. You must consider periodicity and singularities when computing parameter-space boxes for faces. Each PCURVE is constructed and maintained independently of other PCURVEs on a given face; therefore, PCURVEs on a periodic surface or a surface with singularities may not meet end to end, implying that PCURVEs on such a surface do not necessarily form a closed loop in parameter space. Other Geometry-related Topics Before we present a couple C++ examples demonstrating the use of geometry in ACIS, there are a few more geometry-related topics we would like to discuss. Continuity Requirements Main article: Continuity Many of the algorithms in ACIS are more efficient if curves and surfaces are smooth. It is preferable to have continuous second derivatives for these algorithms; that is, curves and surfaces should be C2; however, the minimum continuity requirement for geometry is that it must be at least G1. One algorithm that requires greater than G1 continuity is the algorithm for mass property evaluations, which expects curves to be C1. Discontinuity information is recorded within the intcurve, int_cur, spline, and spl_sur classes. Senses A number of the geometry and topology classes we have discussed have sense information in them. Sense information allows an object to be in either the same direction or the opposite direction as the object underlying it. For instance, a FACE may be oriented in the same direction or the opposite direction as the SURFACE underlying it. If you are evaluating the normal direction at a point on a FACE, you must reverse the direction of the SURFACE normal if the FACE is reversed with respect to the SURFACE. The sense of the FACE is obtained by the FACE::sense member function. This method returns a REVBIT whose value is either FORWARD or REVERSED. In addition to FACEs, several other classes in ACIS contain sense information. The most commonly used classes that contain sense information are listed below. Object With Regards to Object Sense-Determining Function FACE SURFACE REVBIT FACE::sense() spline spl_sur logical spline::reversed() COEDGE EDGE REVBIT COEDGE::sense() EDGE CURVE REVBIT EDGE::sense() intcurve int_cur logical intcurve::reversed() PCURVE bs2_curve int PCURVE::index() (If index is -1 or -2, the PCURVE is reversed with regards to the bs2_curve.) pcurve par_cur logical pcurve::reversed() To demonstrate how senses are used, let's look at how one might obtain the 3-dimensional location of a point 25% of the parametric distance along a COEDGE. To obtain the parameter range of the COEDGE, we must first obtain the parameter range of its EDGE, and reverse it if the COEDGE is reversed with respect to the EDGE. We must next find the parameter value 25% of the way between the low and high parameter values. We must then map this COEDGE parameter into a curve parameter, taking into account any reversals between the EDGE and CURVE, and between the COEDGE and EDGE. We finally evaluate the curve at the appropriate curve parameter. In C++, this all translates into the following: SPAinterval edge_range = my_coedge->edge()->param_range(); SPAinterval coedge_range = (my_coedge->sense() == FORWARD) ? edge_range : -edge_range; double coedge_param = coedge_range.interpolate(0.25); double curve_param = (my_coedge->sense() == my_coedge->edge()->sense()) ? coedge_param : -coedge_param; SPAposition loc = my_coedge->edge()->geometry()->equation().eval_position(curve_param); Of course, you could also simply call coedge_param_pos; in fact, this is the preferred approach because the code snippet above does not properly handle tolerant coedges. How would you find the parameter-space location corresponding to the same point on a coedge? (There are multiple solutions to this question.) Faces on Periodic Surfaces Different applications have different requirements for representing faces on periodic spline surfaces. Some applications can represent only open spline faces. These applications require closed or periodic faces to be split into two faces. Other applications can represent a face on a periodic spline surface only if the face contains a seam edge that splits the face in the periodic direction (that is, a periodic spline face must have a peripheral loop). Still other applications can represent a face on a periodic spline surface without a seam edge (that is, a periodic spline face does not need to have a peripheral loop). ACIS can maintain any of these representations. Prior to R16, ACIS could not represent periodic spline faces without seam edges. The requirement for a seam edge on a periodic spline face is controlled by the periodic_no_seam option. If this option is TRUE, seams are not required; if it is FALSE, seams are required. (Options are described in more detail in Tutorials (Options).) If an application requires open faces, a periodic spline face with a seam edge can be split again along an isoparameter line using api_split_face. Periodic faces can be split using api_split_periodic_faces. The new_periodic_splitting option controls the number and location of splits performed by api_split_periodic_faces. Using a Subset of a Curve or Surface Main article: Trimming, Subsetting, and Extending Geometry Often an ENTITY or algorithm will use only a portion of a curve or a surface. To support these cases, ACIS allows a "subset range" to be associated with a curve or surface. Subsetting is generally preferable to trimming the underlying geometry because it makes subsequent comparisons among multiple subsets more efficient. In addition, subsetting geometry is faster than trimming geometry, though it does not have the potential to reduce the amount of memory required by a geometric entity. The following subsetting-related member functions apply to both curves and surfaces. Member Function Description limit subsets a curve or surface in place subset creates a subsetted copy of the curve or surface subsetted determines whether a curve or surface has been subsetted or not unlimit removes the subset range from a curve or surface unsubset creates an unsubsetted copy of the curve or surface Note: param_range returns the subsetted parameter range of a curve or surface. Similarly, functions that return the form or period of a curve or surface also take subsetting into account. To examine the parameter range or form of the unsubsetted curve or surface, you must unlimit the curve or surface, perform the query, and then relimit the curve or surface. Relations among Parameter Ranges There are some requirements for the relationships among the parameter ranges of geometrical and topological entities. For instance, the parameter range of a FACE must be contained within the parameter range of the FACE's SURFACE. Conversely, the parameter range of a SURFACE must contain the parameter range of all the FACEs that reside on the SURFACE. Two FACEs on a SURFACE Similarly, the parameter range of an EDGE must be contained within the parameter range of the EDGE's CURVE, and the parameter range of a CURVE must contain the parameter range of all the EDGEs that reside on the CURVE. Three EDGEs on a CURVE The parameter range of a COEDGE must be contained within the parameter range of the COEDGE's PCURVE, and the parameter range of a PCURVE must contain the parameter range of all the COEDGEs that reside on the PCURVE. In addition, the parameter range of a PCURVE must be contained within the parameter range of its underlying intcurve or bs2_curve, and the parametric bounds of a PCURVE (that is, its SPApar_box) must be contained within the parameter range of its underlying surface. Three COEDGEs on a PCURVE Because of these relationships, there is a trade-off between aggressively subsetting geometry and leaving it unsubsetted so that it can support multiple topological entities. Use Counting Main article: Use Counting Many classes of objects in ACIS are shared, and are therefore use-counted. Sharing objects within the data structure makes the model memory requirements less, reduces the size of saved files, and makes comparisons more efficient. Although use counting is fairly hidden from the application developer, it is useful to understand which objects in ACIS are use counted. The following is a list of the classes in ACIS that we have talked about that are use counted. • APOINT • CURVE • SURFACE • PCURVE • int_cur • spl_sur • par_cur Use-counted objects are typically not destructed or lost by the application developer, but rather the use counts are incremented with each use and decremented when the owning object is deleted or lost. When the use count goes to zero, the object can be destructed or lost. Transformations Main article: Transformation Transformations are used to change the position or orientation of objects within an ACIS model. Transformations may contain translation, rotation, scaling, reflection, and shear components. The most commonly used are probably translation and rotation. If translations and/or rotations cause an object to be moved by a very large distance, a loss of numerical precision can occur. Scaling can also cause geometric problems: small geometric inconsistencies can be magnified so they are no longer insignificant, and small features can be reduced so they are no longer significant. Reflection will cause relative directions to change and shear can cause geometric types to change. Application developers may want to restrict the transformation components available to their end users. Transformations are used in two ways in ACIS. A transformation can be attached to a BODY and used by subsequent operations, or the transformation can be applied to the geometry of all the subordinate ENTITYs under the BODY. Both approaches have advantages. Simply attaching a transformation to a BODY allows multiple transformations to be applied to the BODY without affecting the underlying geometry. This can reduce the "stack-up" error commonly associated with repeatedly applying transformations. The disadvantage of this approach is that any geometric evaluations must take the transformation into account. (Most geometric evaluation functions take optional transformation arguments for this purpose.) Applying a transform to the geometry of all the subordinate ENTITYs under the BODY allows subsequent geometric evaluations to be done more efficiently (and may make debugging geometric problems more efficient); however, it also takes time to apply a transform, and repeated application of transforms can cause "stack-up" errors. Application developers should attempt to use transformations as efficiently as possible. From an application developer's perspective, transformations are implemented using two classes: TRANSFORM and SPAtransf. The TRANSFORM class is derived from ENTITY, and is therefore part of the persistent ACIS model. Each TRANSFORM contains a SPAtransf, which contains the mathematical details of the transformation. The following table describes the most commonly used transformation-related functions. Function Description SPAtransf translate_transf(double x, double y, double z) Creates a SPAtransf that represents a translation. SPAtransf rotate_transf(double angle, SPAvector const &axis) Creates a SPAtransf that represents a rotation. SPAtransf scale_transf(double factor) Creates a SPAtransf that represents a scaling. (If non-uniform scaling or shear is present, this uses the Space Warping component.) SPAtransf reflect_transf(SPAvector const &normal) Creates a SPAtransf that represents a reflection. SPAtransf shear_transf(double xy, double xz, double yz) Creates a SPAtransf that represents a shearing. (This uses the Space Warping component.) SPAtransf const & SPAtransf::operator*=(SPAtransf const &) Concatentate two SPAtransfs. SPAtransf SPAtransf::inverse() const Creates the inverse of the given SPAtransf. api_apply_transf(BODY*, SPAtransf const &) Creates and attaches a TRANSFORM to a BODY. api_change_body_trans(BODY*, NULL,…) If the second argument of this function is NULL, it will apply the current TRANSFORM to all the geometry in the BODY, leaving a NULL TRANSFORM on the BODY. ACIS modeling transformations should not be used for viewing purposes. Application developers should distinguish between modeling and viewing transformations. Converting Analytic Geometry into B-spline Geometry Main article: Spline Conversion It is possible to convert ACIS analytic curves and surfaces into NURBS geometry using api_convert_to_spline . Although analytic curves and surfaces can be precisely represented by NURBS geometry, there will be a parameterization change. (For instance, the ACIS representation of a circle has constant velocity parameterization; however, a NURBS representation of a circle does not.) The behavior of this function (in particular its splitting of periodic faces) is controlled by the new_periodic_splitting and split_periodic_splines options. It is also possible to convert intcurve and spline geometry (including procedural curves and surfaces) into analytic curves and surfaces, using api_simplify_entity(ENTITY*&, ...). This function converts all intcurves and splines that are within a specified tolerance of an analytic curve or surface. This function may convert non-tolerant edges and vertices into tolerant edges and vertices. Validity Checking Main article: ACIS Checker The geometry and topology of an ACIS topological entity and its subordinate entities can be checked for inconsistencies and invalidities using api_check_entity. The amount of checking done is controlled by the check_level option. The higher the check level, the more checking that is performed. Extensive geometry checking is performed at check level 30. There are several other options that affect the checking behavior. For more information about validity checking and the options that affect it, refer to ACIS Checker. We suggest that models be checked (and any necessary repairs be made) whenever a model is imported from another system or a previous version of ACIS. Checking should not be required during an ACIS modeling session. Thorough validity checking is time-consuming and definitely should not be done after every operation. Tutorials (Importing Models) will discuss repairing problems identified by validity checking. C++ Example Now that we have introduced you to ACIS topology, geometry, and some math classes, let's put this all together in an example that creates a very simple model and interrogates it. We shall start by creating a wire body that consists of four straight edges that form a closed loop. We shall cover this wire body with a face, thereby converting the wire body into a sheet body. The geometry underlying the face will be a bilinear b-spline surface. We shall then split the face along an isoparametric curve. We shall make geometric queries of the model before and after the face split operation to exercise our mathematical classes. The figure below shows the geometry we will create and query. Two faces on a bilinear b-spline surface C++ Example #include <stdio.h> #include "acis.hxx" #include "api.hxx" #include "api.err" #include "kernapi.hxx" #include "cstrapi.hxx" #include "coverapi.hxx" #include "boolapi.hxx" #include "lists.hxx" #include "alltop.hxx" #include "get_top.hxx" #include "faceutil.hxx" #include "param.hxx" #include "vector.hxx" #include "unitvec.hxx" #include "surface.hxx" #include "surdef.hxx" #include "box.hxx" #include "getbox.hxx" // Declaration of the ACIS licensing function. void unlock_spatial_products_<NNN>(); // Declaration of our functions. int do_something(); outcome my_create_sheet_body(BODY*&); void my_debug_face(FACE*); int my_initialization(); int my_termination(); // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and propagate the error. #define CHECK_RESULT \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ sys_error(err_no); \ } // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and return the error number. #define CHECK_RESULT2 \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ return err_no; \ } // The main program... int main (int argc, char** argv) { int ret_val = my_initialization(); if (ret_val) return 1; ret_val = do_something(); ret_val = my_termination(); if (ret_val) return 1; printf("Program completed successfully\n\n"); return 0; } int do_something(){ API_BEGIN // First, create a sheet body consisting of a single face. BODY * my_body = NULL; result = my_create_sheet_body(my_body); CHECK_RESULT // Let's query the face before we split it. FACE * my_face = my_body->lump()->shell()->face(); printf ("Before splitting the face...\n\n"); my_debug_face(my_face); // Split the face into two faces along an isoparametric curve. // We will perform the split in the u-direction at the mid // parameter of the surface. result = api_split_face (my_face, TRUE, TRUE, 0.5); CHECK_RESULT // We should now have two faces in the body. ENTITY_LIST faces; get_faces (my_body, faces); if (faces.count() != 2) { printf("Unexpected number of faces!\n"); sys_error(API_FAILED); } // Do the faces share the same surface? printf ("Surface underlying 1st face is 0x%x\n", ((FACE*)faces[0])->geometry()); printf ("Surface underlying 2nd face is 0x%x\n\n", ((FACE*)faces[1])->geometry()); // Let's query the faces after the split. printf ("Face 0 after splitting the face...\n\n"); my_debug_face((FACE*)faces[0]); printf ("Face 1 after splitting the face...\n\n"); my_debug_face((FACE*)faces[1]); // We should delete the body before exiting this function. result = api_delent(my_body); CHECK_RESULT API_END return result.error_number(); } outcome my_create_sheet_body(BODY *& my_body) { // This function creates a sheet body consisting of a single, // double-sided face. API_BEGIN // Create a polygonal wire body from an array of positions. // The first point is the same as the last to create a closed wire. // The wire body will contain 4 edges and 4 vertices. SPAposition * pos_array = ACIS_NEW SPAposition[5]; pos_array[0] = SPAposition(-5.0, -5.0, -1.0); pos_array[1] = SPAposition(-5.0, 5.0, 1.0); pos_array[2] = SPAposition( 5.0, 5.0, -1.0); pos_array[3] = SPAposition( 5.0, -5.0, 1.0); pos_array[4] = SPAposition(-5.0, -5.0, -1.0); result = api_make_wire (my_body, 5, pos_array, my_body); CHECK_RESULT ACIS_DELETE [] pos_array; // De-allocate the array. // Convert the wire body into a solid body. // This is accomplished by covering the wire with a face. // The solid body will contain one single-sided face. WIRE * my_wire = my_body->lump()->shell()->wire(); FACE * my_face = NULL; result = api_cover_wire (my_wire, *(surface*)NULL_REF, my_face); CHECK_RESULT // Convert the solid body into a sheet body. // The sheet body will contain one double-sided face. result = api_body_to_2d (my_body); CHECK_RESULT API_END return result; } void my_debug_face(FACE * my_face) { // This function performs a number of queries on the given face. // Examine the parameter ranges of the surface and the face. surface const & surf = my_face->geometry()->equation(); SPApar_box s_range = surf.param_range(); printf("\tsurface u range is (%g, %g)\n", s_range.u_range().start_pt(), s_range.u_range().end_pt()); printf("\tsurface v range is (%g, %g)\n\n", s_range.v_range().start_pt(), s_range.v_range().end_pt()); SPApar_box f_range; logical success = sg_get_face_par_box (my_face, f_range); if (success) { printf("\tface u range is (%g, %g)\n", f_range.u_range().start_pt(), f_range.u_range().end_pt()); printf("\tface v range is (%g, %g)\n\n", f_range.v_range().start_pt(), f_range.v_range().end_pt()); } // Examine the u and v directions at the midpoint of the face. SPApar_pos mid_param = f_range.mid(); SPAposition mid_pos; SPAvector deriv[2]; surf.eval(mid_param, mid_pos, deriv); printf("\tmid_param is (%g, %g)\n", mid_param.u, mid_param.v); printf("\tmid_pos is (%g, %g, %g)\n", mid_pos.x(), mid_pos.y(), mid_pos.z()); printf("\tu derivative is (%g, %g, %g)\n", deriv[0].x(), deriv[0].y(), deriv[0].z()); printf("\tv derivative is (%g, %g, %g)\n", deriv[1].x(), deriv[1].y(), deriv[1].z()); // We could obtain the face normal using surface::eval_normal() // or sg_get_face_normal(), but we already have the u and v // derivatives, so we can just take their cross product. // Remember to reverse the surface normal is the face is reversed. SPAvector surface_normal = deriv[0] * deriv[1]; SPAunit_vector face_dir = normalise((my_face->sense() == FORWARD) ? surface_normal : -surface_normal); printf("\tmid_pt normal direction is (%g, %g, %g)\n\n", face_dir.x(), face_dir.y(), face_dir.z()); // Examine the 3D bounding box of the face. SPAbox f_box = get_face_box(my_face); SPAinterval x_range = f_box.x_range(); SPAinterval y_range = f_box.y_range(); SPAinterval z_range = f_box.z_range(); printf("\tface box x range is (%g, %g)\n", x_range.start_pt(), x_range.end_pt()); printf("\tface box y range is (%g, %g)\n", y_range.start_pt(), y_range.end_pt()); printf("\tface box z range is (%g, %g)\n\n", z_range.start_pt(), z_range.end_pt()); } int my_initialization() { // Start ACIS. outcome result = api_start_modeller(0); CHECK_RESULT2 // Call the licensing function to unlock ACIS. unlock_spatial_products_<NNN>(); // Initialize all necessary components. result = api_initialize_kernel(); CHECK_RESULT2 return 0; } int my_termination() { // Terminate all necessary components. outcome result = api_terminate_kernel(); CHECK_RESULT2 // Stop ACIS and release any allocated memory. result = api_stop_modeller(); CHECK_RESULT2 return 0; } In addition to seeing how the topology, geometry and math classes are used in the above example, some of the output might be of interest. The output of the program should look similar to the following. Program Output Before splitting the face... surface u range is (-5.09902, 5.09902) surface v range is (-5.09902, 5.09902) face u range is (-5.09902, 5.09902) face v range is (-5.09902, 5.09902) mid_param is (0, 0) mid_pos is (0, 0, -2.22045e-016) u derivative is (1.74186e-016, 0.980581, -0) v derivative is (0.980581, -0, -0) mid_pt normal direction is (-0, 0, -1) face box x range is (-5, 5) face box y range is (-5, 5) face box z range is (-1, 1) Surface underlying 1st face is 0x28a9d98 Surface underlying 2nd face is 0x28a9d98 Face 0 after splitting the face... surface u range is (-5.09902, 5.09902) surface v range is (-5.09902, 5.09902) face u range is (-5.09902, 0.0010198) face v range is (-5.09902, 5.09902) mid_param is (-2.549, 0) mid_pos is (-4.44089e-016, -2.4995, -2.22045e-016) u derivative is (1.74198e-016, 0.980581, 0) v derivative is (0.980581, -0, 0.0980385) mid_pt normal direction is (0.099484, -1.76731e-017, -0.995039) face box x range is (-5, 5) face box y range is (-5, 0.001) face box z range is (-1, 1) Face 1 after splitting the face... surface u range is (-5.09902, 5.09902) surface v range is (-5.09902, 5.09902) face u range is (-0.0010198, 5.09902) face v range is (-5.09902, 5.09902) mid_param is (2.549, 0) mid_pos is (4.44089e-016, 2.4995, -2.22045e-016) u derivative is (1.74198e-016, 0.980581, -0) v derivative is (0.980581, -0, -0.0980385) mid_pt normal direction is (-0.099484, 1.76731e-017, -0.995039) face box x range is (-5, 5) face box y range is (-0.001, 5) face box z range is (-1, 1) Program completed successfully  Looking at the output you should observe: • The original face and its underlying surface have identical parameterizations, which are symmetric in the u and v directions. • The u and v derivatives of the original surface point in the +y and +x directions and the face normal points in the -z direction. (The +u direction was obtained from the first edge of the wire body. Can you modify the position array to create a surface with the u and v directions in the +x and +y directions?) • After the split the parameter ranges of the faces and their underlying surfaces are quite different. The surface underlying each face was not trimmed or subsetted to the range of the face. • The faces after the split share the same underlying surface. • If the u parameter ranges of the faces after the split were combined, they would be equivalent to the u parameter range of the face before the split. • Although the face was split along the u=0 parameter line, the u parameter ranges of the faces after the split do not fall precisely along the u=0 parameter line. They are slightly larger than that. • If the 3D bounding boxes of the faces after the split were combined, they would be equivalent to the 3D bounding box of the face before the split. However, notice that each of the bounding boxes of the split faces is slightly larger than 1/2 the original bounding box. This is because the parameter ranges of the faces are slightly larger than 1/2 the original face. Modifying the C++ Example Let's modify the previous example program to examine one edge and its coedges. We will use the same geometry as in the previous example and look at the edge that separates the two faces. In particular, we want to test our coedge evaluation code from the Senses section and compare it with the result from coedge_param_pos. In addition, we will present two solutions to the question in the Senses section regarding how you would determine the parameter space location corresponding to the 3D location. Modified C++ Example #include <stdio.h> #include "acis.hxx" #include "api.hxx" #include "api.err" #include "kernapi.hxx" #include "cstrapi.hxx" #include "coverapi.hxx" #include "boolapi.hxx" #include "lists.hxx" #include "alltop.hxx" #include "get_top.hxx" #include "faceutil.hxx" #include "param.hxx" #include "vector.hxx" #include "unitvec.hxx" #include "surface.hxx" #include "surdef.hxx" #include "curve.hxx" #include "curdef.hxx" #include "pcurve.hxx" #include "pcudef.hxx" #include "geometry.hxx" // Declaration of the ACIS licensing function. void unlock_spatial_products_<NNN>(); // Declaration of our functions. int do_something(); outcome my_create_sheet_body(BODY*&); void my_debug_edge(BODY*); void my_debug_coedge(COEDGE*); int my_initialization(); int my_termination(); // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and propagate the error. #define CHECK_RESULT \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ sys_error(err_no); \ } // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and return the error number. #define CHECK_RESULT2 \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ return err_no; \ } // The main program... int main (int argc, char** argv) { int ret_val = my_initialization(); if (ret_val) return 1; ret_val = do_something(); ret_val = my_termination(); if (ret_val) return 1; printf("Program completed successfully\n\n"); return 0; } int do_something(){ API_BEGIN // First, create a sheet body consisting of a single face. BODY * my_body = NULL; result = my_create_sheet_body(my_body); CHECK_RESULT // Split the face into two faces along an isoparametric curve. // We will perform the split in the u-direction at the mid // parameter of the surface. FACE * my_face = my_body->lump()->shell()->face(); result = api_split_face (my_face, TRUE, TRUE, 0.5); CHECK_RESULT // Let's examine the edge between the two split faces. my_debug_edge(my_body); // We should delete the body before exiting this function. result = api_delent(my_body); CHECK_RESULT API_END return result.error_number(); } outcome my_create_sheet_body(BODY *& my_body) { // This function creates a sheet body consisting of a single, // double-sided face. API_BEGIN // Create a polygonal wire body from an array of positions. // The first point is the same as the last to create a closed wire. // The wire body will contain 4 edges and 4 vertices. SPAposition * pos_array = ACIS_NEW SPAposition[5]; pos_array[0] = SPAposition(-5.0, -5.0, -1.0); pos_array[1] = SPAposition(-5.0, 5.0, 1.0); pos_array[2] = SPAposition( 5.0, 5.0, -1.0); pos_array[3] = SPAposition( 5.0, -5.0, 1.0); pos_array[4] = SPAposition(-5.0, -5.0, -1.0); result = api_make_wire (my_body, 5, pos_array, my_body); CHECK_RESULT ACIS_DELETE [] pos_array; // De-allocate the array. // Convert the wire body into a solid body. // This is accomplished by covering the wire with a face. // The solid body will contain one single-sided face. WIRE * my_wire = my_body->lump()->shell()->wire(); FACE * my_face = NULL; result = api_cover_wire (my_wire, *(surface*)NULL_REF, my_face); CHECK_RESULT // Convert the solid body into a sheet body. // The sheet body will contain one double-sided face. result = api_body_to_2d (my_body); CHECK_RESULT API_END return result; } void my_debug_edge(BODY * my_body) { // This function performs a number of queries on the edge // between the two split faces. // First, let's find the correct edge. ENTITY_LIST edges; get_edges(my_body, edges); EDGE * my_edge = NULL; int num_edges = edges.count(); for (int i = 0; i < num_edges; i++) { EDGE * this_edge = (EDGE*) edges[i]; if (this_edge->coedge()->partner() != NULL) { my_edge = this_edge; break; } } // Let's look at 5 positions along this edge. const int num_pos = 5; double start_param = my_edge->start_param(); double end_param = my_edge->end_param(); double incr = (end_param - start_param) / (num_pos - 1); const curve & my_curve = my_edge->geometry()->equation(); for (int i_pos = 0; i_pos < num_pos; i_pos++) { double this_param = start_param + i_pos * incr; SPAposition loc = my_curve.eval_position(this_param); printf("edge location[%d] = (%.2f, %.2f, %.2f)\n", i_pos, loc.x(), loc.y(), loc.z()); } // Let's examine both coedges of this edge. my_debug_coedge(my_edge->coedge()); my_debug_coedge(my_edge->coedge()->partner()); } void my_debug_coedge(COEDGE * my_coedge) { // Let's look at 5 positions and 5 parametric locations // along this coedge. printf("\nThis coedge is %s\n", (my_coedge->sense() == FORWARD) ? "FORWARD" : "REVERSED"); // We shall compare the 3D positions along the coedge // as calculated by two methods: // (1) the approach described in the "Senses" subsection, // (2) coedge_param_pos(). // These should provide identical results. const int num_loc = 5; SPAinterval edge_range = my_coedge->edge()->param_range(); SPAinterval coedge_range = (my_coedge->sense( ) == FORWARD) ? edge_range : -edge_range; double incr = 1.0 / (num_loc - 1); double max_diff = 0.0; for (int i = 0; i < num_loc; i++) { double coedge_param = coedge_range.interpolate(i * incr); double curve_param = (my_coedge->sense() == my_coedge->edge()->sense()) ? coedge_param : -coedge_param; SPAposition loc = my_coedge->edge()->geometry()->equation() .eval_position(curve_param); printf("coedge location[%d] = (%.2f, %.2f, %.2f)\n", i, loc.x(), loc.y(), loc.z()); // Alternate calculation method SPAposition loc2 = coedge_param_pos(my_coedge, coedge_param); double diff = (loc2 - loc).len(); if (diff > max_diff) max_diff = diff; } printf("maximum 3D deviation = %g\n", max_diff); // We shall compare the parametric locations along the coedge // as calculated by two methods: // (1) inverting the 3D location, // (2) evaluating the pcurve under the coedge. // These could provide different results because the pcurve // is just an approximation. const surface & my_surf = my_coedge->loop()->face() ->geometry()->equation(); pcurve my_pcurve = my_coedge->geometry()->equation(); max_diff = 0.0; for (int ii = 0; ii < num_loc; ii++) { double coedge_param = coedge_range.interpolate(ii * incr); SPApar_pos p_loc = my_pcurve.eval_position(coedge_param); printf("coedge location[%d] = (%.2f, %.2f)\n", ii, p_loc.u, p_loc.v); // Alternate calculation method SPAposition loc = coedge_param_pos(my_coedge, coedge_param); SPApar_pos p_loc2 = my_surf.param(loc); double diff = (p_loc2 - p_loc).len(); if (diff > max_diff) max_diff = diff; } printf("maximum parametric deviation = %g\n", max_diff); } int my_initialization() { // Start ACIS. outcome result = api_start_modeller(0); CHECK_RESULT2 // Call the licensing function to unlock ACIS. unlock_spatial_products_<NNN>(); // Initialize all necessary components. result = api_initialize_kernel(); CHECK_RESULT2 return 0; } int my_termination() { // Terminate all necessary components. outcome result = api_terminate_kernel(); CHECK_RESULT2 // Stop ACIS and release any allocated memory. result = api_stop_modeller(); CHECK_RESULT2 return 0; } The output from the modified program should be similar to the following. Program Output edge location[0] = (5.00, 0.00, -0.00) edge location[1] = (2.50, 0.00, -0.00) edge location[2] = (0.00, 0.00, -0.00) edge location[3] = (-2.50, 0.00, -0.00) edge location[4] = (-5.00, 0.00, -0.00) This coedge is FORWARD coedge location[0] = (5.00, 0.00, -0.00) coedge location[1] = (2.50, 0.00, -0.00) coedge location[2] = (0.00, 0.00, -0.00) coedge location[3] = (-2.50, 0.00, -0.00) coedge location[4] = (-5.00, 0.00, -0.00) maximum 3D deviation = 0 coedge location[0] = (0.00, 5.10) coedge location[1] = (0.00, 2.55) coedge location[2] = (0.00, 0.00) coedge location[3] = (0.00, -2.55) coedge location[4] = (0.00, -5.10) maximum parametric deviation = 0 This coedge is REVERSED coedge location[0] = (-5.00, 0.00, -0.00) coedge location[1] = (-2.50, 0.00, -0.00) coedge location[2] = (0.00, 0.00, -0.00) coedge location[3] = (2.50, 0.00, -0.00) coedge location[4] = (5.00, 0.00, -0.00) maximum 3D deviation = 0 coedge location[0] = (0.00, -5.10) coedge location[1] = (0.00, -2.55) coedge location[2] = (0.00, 0.00) coedge location[3] = (0.00, 2.55) coedge location[4] = (0.00, 5.10) maximum parametric deviation = 0 Program completed successfully  This example should illustrate how evaluations and inverse evaluations can be made in ACIS. We see how positions calculated along an edge correspond to positions calculated along its coedges. We see that coedge evaluation code presented in the Senses section produces results equivalent to those produced by coedge_param_pos. In addition, we see that we can obtain parameter space locations along a coedge by either: (a) inverse evaluations of positions on the surface, or (b) evaluations of the pcurve. The pcurve-based evaluations could contain some approximation error, but in our example they did not. Tutorial 6: Tolerances Global Tolerances One of the keys to a robust geometric modeling system is consistency. And, one of the keys to consistency in a geometric modeling system is using one tolerance for all geometric calculations. Ahhh. If only life were that simple. ACIS does use one tolerance for 99% of its geometric calculations. That tolerance is SPAresabs, or simply resabs (pronounced "rez-abs") for short. This is the tolerance used when comparing two positions in the 3-dimensional model space. If two positions are within resabs of each other, they may be considered the same position. Of course, what does it mean for "two positions to be within resabs of each other?" If two positions are precisely resabs apart, are they within resabs of each other? How would you manage the imprecision associated with floating-point arithmetic on computers? The answer to these questions is: it does not really matter as long as you are consistent. To be consistent, you must always perform the same comparison in the same way. In ACIS, if you want to compare two positions using a resabs tolerance you simply use operator==(SPAposition const&, SPAposition const&) or same_point(SPAposition const&, SPAposition const&). But how can you perform geometric calculations in a 1-dimensional or 2-dimensional parameter space using a 3-dimensional model-space tolerance? The answer is relatively straightforward: you map the 1-dimensional or 2-dimensional parameter-space values into the 3-dimensional model-space and perform positional comparisons in the 3-dimensional model-space. If this is done, all of your geometric modeling calculations will be done consistently. Occasionally, you will want to compare two vectors instead of two positions. How can you compare the directions of two vectors if all ACIS has is a positional tolerance? Well, just for this purpose ACIS has another tolerance, SPAresnor, or resnor (pronounced "rez-nor") for short. This tolerance is used when comparing the directions of two vectors. If the directions of two vectors are within resnor of each other, the vectors may be considered parallel. As with positions, consistency matters, so all comparisons should be performed using one of the functions parallel(SPAvector const&, SPAvector const&), antiparallel(SPAvector const&, SPAvector const&), biparallel(SPAvector const&, SPAvector const&), or perpendicular(SPAvector const&, SPAvector const&). We hope that you are beginning to see that the ACIS math classes are not only convenient, but also provide a means of performing consistent calculations -— which makes ACIS and your application more robust. But wait a minute! Initially we said that we wanted to use a single tolerance for all calculations, and then we introduced a second tolerance for vectors. If some geometric calculations are made with one tolerance and other geometric calculations are made with another tolerance, how can we still expect to produce consistent results? The answer is simple: resnor depends on resabs. In order to understand the relationship between resnor and resabs, we must introduce a measure of the longest distance in the model. If you think of a bounding box surrounding your model, the longest possible distance is the length of the longest diagonal. So, if we have two vectors that are the length of the longest possible vector in your model and they are parallel to each other to within a tolerance, what does this mean? Let's look at the figure below in which we have set the maximum deviation between the two vectors to be resabs. Two Long Vectors that Deviate by resabs If we want the maximum possible deviation between the two vectors to be resabs, and the vectors are the longest length possible, what is the maximum angle between them? The angle resnor. So what is the relationship between resnor and resabs? $\mbox{resnor} = \left ( \frac{ \mbox{resabs} } { \mbox{longest dist} } \right )$ We can of course rearrange the equation to: $\mbox{longest dist} = \left ( \frac{ \mbox{resabs} } { \mbox{resnor} } \right )$ This means that if we have a pair of values for resabs and resnor, we have effectively set the longest distance in the model. But if we constrain ourselves to using that as the longest distance in the model, we can use resnor for angular measurements and know that vector displacements will be less than resabs. In other words, if our model is less than some calculable size, then we still are using one tolerance for all our geometric calculations. At this point, we should probably state what we mean by "all our geometric calculations." We really mean geometric comparisons. We do not mean to imply that resabs ever is used for a convergence criterion in an iterative geometric algorithm. In fact, quite the opposite is true. Geometric algorithms in ACIS iterate to criteria many orders of magnitude tighter than resabs. This reduces the possibility of tolerance stack-up errors. What does this mean for you, the user? When you evaluate a geometric quantity using ACIS, the error associated with the calculations of that quantity is well within resabs. But, you might ask, are there ever times when something does not need to be calculated that precisely? When an approximation will do? Yes. ACIS does use B-spline approximations for some types of procedural curves and surfaces in order to speed up algorithms. This does not mean that the procedural curves and surfaces are represented as B-splines. There just may be B-spline approximations to make some algorithms more efficient. In fact, developers of some algorithms, such as algorithms to display graphics, may choose to use only the approximations rather than the precise representations, for increased efficiency. B-spline approximations are generally calculated using a tolerance of SPAresfit, or simply resfit. For the sake of completeness, let us finally mention that there is one additional tolerance in ACIS, SPAresmch, or simply resmch (pronounced "rez-machine"). This tolerance is seldom used. It is the tolerance associated with floating-point calculations on a double-precision computer. Because of the numerical stack-up, it is set a few orders of magnitude greater than the precision of a double-precision word. This value does not vary with various platforms, but rather is kept constant to achieve consistent results among them. The default values given to these four tolerances are: Tolerance type Value SPAresabs 1e-6 SPAresnor 1e-10 SPAresfit 1e-3 SPAresmch 1e-11 Although SPAresabs, SPAresnor, SPAresfit, and SPAresmch are variables, we would strongly recommend that you do not change their values in your application. Changing their values could significantly affect the robustness of your application. For more information on global tolerances, refer to Tolerance Variables, Scaling Tolerance Variables, and Dynamic Range. Aside: Choosing Units for an Application If SPAresabs, SPAresnor, SPAresfit, and SPAresmch should not be changed, how can applications represent models with a wide range of sizes? ACIS is unit-less. ACIS does not have any concept of an inch, or a meter, or a mile. The units that you, the developer, choose to use in your application, or the end user is allowed to use in his session, are up to you. Furthermore, the units that the end user sees do not need to be the units used in the internal representation. For example, assume you are designing an application that needs to model the air space over the continental United States. The longest distance in the continental U.S. is less than 3000 miles, or 5000 kilometers. If longest_dist = resabs / resnor, then longest_dist = 10,000. So, if we represent the data using kilometers, we shall never have a vector longer than 10,000 units. The absolute resolution of the model (resabs) would be 10-6 kilometer, or 1 millimeter, which is probably sufficient for any application modeling the entire United States. The user interface could present information to the user in whatever units were most appropriate: for example, kilometers, miles, or feet. Another way of looking at resnor is that it sets the dynamic range of the model. It is the ratio of the smallest representable distance to the largest representable distance. The exponent of its inverse (10) is a measure of the number of significant figures in the data. In other words, the data in an ACIS model has 10 significant figures. This is just a function of word sizes on 32-bit computers. (You could design an application that modeled assemblies, in which each object instanced in the assembly was an ACIS model, and in this case the assembly could appear to have more than 10 significant figures, but the data in each ACIS model would have only 10 significant figures.) Local Tolerances (Tolerant Entities) Data in ACIS models will sometimes not meet ACIS global tolerances. This typically occurs when models are imported from other systems, but occasionally it occurs during ACIS modeling operations. For instance, you might create two bodies, each of which meets the ACIS global tolerances, but which may be oriented such that their combination would not meet these tolerances. Faces, edges, and vertices on the two bodies may be nearly coincident, but not precisely coincident. How does ACIS deal with these types of situations? In these cases, entities that do not meet global tolerances can be represented using tolerant entities. If a VERTEX does not lie on an EDGE to within SPAresabs, the VERTEX can be represented as a TVERTEX, which contains a local tolerance that specifies the maximum positional error of the TVERTEX. If an EDGE does not lie on a FACE to within SPAresabs, the EDGE can be represented as a TEDGE, which contains a local tolerance that specifies the maximum positional error of the TEDGE. All ACIS algorithms should be able to process TVERTEXes and TEDGEs. This means that if the algorithm encounters a tolerant entity, it uses the tolerance contained within the ENTITY rather than SPAresabs for positional comparisons. In addition, some ACIS algorithms may propagate tolerant entities. As an example, if you were to offset a body with tolerant edges and vertices, you should expect that the offset body will also contain tolerant edges and vertices. What are the preferred ways to make positional comparisons using tolerant entities? 1) The best way is to use an ACIS API function. ACIS contains many functions that can aid you in making positional comparisons. Perhaps the most obvious is the Boolean function for comparing two bodies, api_boolean. But there is another function that makes this comparison more efficient if you just want to know whether the bodies are separate or intersecting: api_clash_bodies. The table below contains some of the most frequently used API functions that you can use to make geometric comparisons. Function Description api_boolean Can be used to generate the intersection of two bodies. api_clash_bodies Determines if two BODYs "clash" and, optionally, the nature of the clash. api_clash_faces Determines if two FACEs "clash" and the nature of the clash. api_edent_rel Determines the spatial relationship between an EDGE and another ENTITY. api_edfa_int Generates the intersection between an EDGE and a FACE. api_entity_entity_distance Determines the minimum distance between two ENTITYs. api_entity_point_distance Determines the distance between a point and an ENTITY. api_fafa_int Generates the intersection between two FACEs. api_find_cls_ptto_face Determines the point on a FACE nearest a given point. api_find_vertex Determines the VERTEX on a BODY nearest a given point. api_intersect Generates the intersection between two BODYs. api_intersect_curves Determines the intersection between two EDGEs or their underlying curves. api_planar_slice Generates the wire BODY representing the intersection between a plane and a BODY. api_point_in_body Determines whether a point lies inside, outside, or on the boundary of a BODY. api_point_in_face Determines whether a point lies inside, outside, or on the boundary of a FACE. The point is assumed to lie on the surface of the FACE. api_ptent_rel Determines the spatial relationship between a point and an ENTITY. 2) If you must make positional comparisons in your code... a) If you are comparing a SPAposition with an EDGE (or VERTEX), the tolerance value should be the maximum of the value returned by EDGE::get_tolerance (or VERTEX::get_tolerance) and SPAresabs. For example, when comparing a SPAposition with the vertex vert, you would define this value as follows: double tol = (vert->get_tolerance( ) > SPAresabs) ? vert->get_tolerance( ) : SPAresabs; b) If you are comparing two EDGEs, two VERTEXes, or an EDGE and a VERTEX, the tolerance value should be the maximum tolerance value returned by EDGE::get_tolerance( ) or VERTEX::get_tolerance( ), or SPAresabs. For example, when comparing the two vertices vert1 and vert2, you would use the following value: double tol = (vert1->get_tolerance( ) > vert2->get_tolerance( )) ? vert1->get_tolerance( ) : vert2->get_tolerance( ); if (tol < SPAresabs) tol = SPAresabs; Note that TEDGES have TCOEDGES, and TCOEDGES exist only in TEDGES. The principal differences between a COEDGE and a TCOEDGE data structures are: • each TCOEDGE must have a PCURVE, • each TCOEDGE has a parameter range, • each TCOEDGE has a parameter-space box, and • each TCOEDGE has an associated 3D CURVE, which represents the 3D image of the PCURVE. The relationship between TEDGEs and TCOEDGEs differs from the relationship between EDGEs and COEDGEs. With non-tolerant ENTITYs, we assumed that all FACEs on an EDGE met precisely (or within SPAresabs). In other words, if an EDGE bounded multiple FACEs, the surfaces underlying all of the FACEs intersected precisely along a single curve, the curve underlying the EDGE. With tolerant ENTITYs, we do not make this assumption. The boundaries of the FACEs do not need to meet precisely along a curve. The 3D curves associated with each TCOEDGE of a given TEDGE do not need to be to be coincident (or within SPAresabs). This fundamentally changes the way evaluations are performed. For instance, if we were evaluating points around a loop on a face and that face had tolerant edges, we would evaluate positions along each tolerant coedge using the parameter range and 3D CURVE of the TCOEDGE, not the parameter range and 3D CURVE of the TEDGE. To demonstrate how evaluations on TCOEDGEs differ from those on non-tolerant COEDGEs, let's look at how one might obtain the 3-dimensional location of a point 25% of the parametric distance along a TCOEDGE. (This can be compared with the discussion in Tutorials (Geometry), which looks at the evaluation of a non-tolerant COEDGE.) The parameter range of the TCOEDGE must be obtained directly from the TCOEDGE. We then find the parameter value 25% of the way between the low and high parameter values and evaluate the TCOEDGE's 3D CURVE at this parameter. In C++ this looks like: SPAinterval coedge_range = my_tcoedge->param_range( ); double coedge_param = coedge_range.interpolate(0.25); SPAposition loc = my_tcoedge->get_3D_curve()->equation().eval_position(coedge_param); Of course, as with non-tolerant coedges, you could simply call coedge_param_pos. We still have not discussed the meaning of the tolerances on TEDGEs and TVERTEXes. The tolerance on a TEDGE represents the maximum distance between the CURVE underlying the TEDGE and the CURVEs on the TCOEDGEs. The tolerance on a TVERTEX represents the maximum distance between the location of the APOINT underlying the TVERTEX and the ends of the EDGE or TEDGE bounded by the TVERTEX. Tutorial 7: Options The behavior of ACIS can be modified using global options. What Is a Global Option? A global option is more like a global variable than an argument to a function. It affects the behavior of ACIS modally, until the value of the option is modified again. The default values for global options are set the way most users prefer the functionality to behave, but there may be cases in which some users would prefer the alternative functionality, so the options exist. ACIS maintains a list of options. Each option has a keyword and a value. Options may be obtained using their keyword. The documentation provides a list of the options whose values are available for user modification. In addition to the options described in the documentation there are some undocumented options. Undocumented options are for system use and their values should not be modified. You should also be aware that some options affect only the behavior of the Scheme Aide and do not exist in the ACIS libraries. These Scheme-only options are also described in the documentation. Effects of Using Options Before we get into the mechanics of options, let's give an example of an option so you have a better understand of what we are talking about. There is an option called periodic_no_seam. The value of this option specifies whether or not periodic spline faces require a seam edge. If a seam edge is required, every periodic spline face must have a periphery loop. If a seam edge is not required, periodic spline faces may be bounded by two separation loops. The default value for periodic_no_seam is TRUE, which means that periodic spline faces do not require a seam edge. However, some applications do not support periodic spline faces without seam edges, so those applications should modify the value of this option, probably immediately after ACIS initialization. Another example of a global option is the merge option. The merge option affects the behavior of regularized Boolean operations and sweep operations. If the value of the merge option is TRUE, its default value, merging of topology with common geometry will occur as part of regularized Boolean and sweep operations. If it is FALSE, merging will not occur. Some applications may desire merging to occur during some operations but not during others. In this case those applications could set the merge option to FALSE before the operation and reset it to TRUE after the operation. In the examples above both options have logical values, either TRUE or FALSE, but options may have integer, floating point, or string values as well. Usage Options are implemented using the option_header class. An application developer may search for a particular option_header using its keyword and the direct interface function find_option. The member function one would call to obtain the value of an option depends on the type of the option (logical, integer, double, or string). • To obtain the value of a logical option one uses option_header::on, which returns TRUE or FALSE. • To obtain the value of an integer option one uses option_header::count, which returns the integer value of the option. • To obtain the value of a double option one uses option_header::value, which returns the double value of the option. • To obtain the value of a string option one uses option_header::string, which returns the string value of the option. There are multiple ways to modify the value of an option. Two of the most frequently used ways are to use the member functions push and pop, or set and reset. The methods push and pop treat the option value as a stack. Values are pushed onto the stack and popped off the stack. Alternatively, one may repeatedly set the value of an option, which does not push new values onto the stack. Calling reset will reset the value of the option to its default value, regardless of whether set or push was used to modify its value. The use of find_option, push, and pop is demonstrated in the code snippet below. Notice that the call to pop is after the API_END statement. This ensures that the value will always be popped, even in the event of an exception. option_header* merge_option = NULL; API_BEGIN merge_option = find_option("merge"); if (merge_option != NULL) merge_option->push(FALSE); // Perform your algorithm API_END if (merge_option != NULL) merge_option->pop(); The same behavior is demonstrated using set in the following code snippet. option_header* merge_option = NULL; logical prev_merge_option_value; API_BEGIN merge_option = find_option("merge"); if (merge_option != NULL) { prev_merge_option_value = merge_option.on(); merge_option->set(FALSE); } // Perform your algorithm API_END if (merge_option != NULL) merge_option->set(prev_merge_option_value); The style used in second example might be more useful if you were changing the value of an option multiple times and wanted to be sure that the option was always set back to the value it had before entering the API_BEGIN/API_END block. In other words, if you were changing the value of an option multiple times and an exception occurred, you might not know how many times to pop the value; therefore, you must set the value to its initial value. Note: If the initial value were not the default value, you could not reset the value using reset. The option_header class is defined in option.hxx; however, specific instances of options are instantiated within the various components of ACIS. For example, the lop_ff_int option is instantiated within the Local Operations component. If an application were linked without the Local Operations component it would not be able to find or use this option. (This is a not problem because this option only affects the behavior of functions in the Local Operations component.) API Functions ACIS also provides API functions for setting the value of an option. These functions are listed below: • api_set_int_option — when the value is an int or logical • api_set_dbl_option — when the value is a double • api_set_str_option — when the value is a char* (character string) Tutorial 8: Memory Management This tutorial introduces you to one means of allocating and de-allocating memory in an ACIS based application, using the ACIS memory management system for all allocations and de-allocations. This is the preferred means for new users because it provides a consistent and efficient approach for all allocations and de-allocations. Additional information on the ACIS memory management system is available at Memory Management. Local Variables Temporary (locally scoped) variables are created on the stack. The creation of these non-persistent variables follows standard C++ conventions. This applies to ACIS specific classes as well as standard types. These allocations (and subsequent automatic de-allocations) are handled by the operating system and do not utilize the ACIS memory management system. The allocation of local variables is demonstrated below. The SPAvector class is the ACIS implementation of a generalized vector class and is used quite extensively within ACIS. { double value1 = 4.0; double value2 = 2.5; SPAvector vec1(value1, 0.0, 0.0); SPAvector vec2(0.0, value2, 0.0); SPAvector vec3 = vec1 * vec2; // The cross product double dot_prod = vec1 % vec2; // The dot product } Persistent Variables Persistent variables are created on the heap. Allocations and de-allocations of persistent variables can be handled by the ACIS memory management system. These variables may be created on an ACIS "free list." (A "free list" is a fast allocation strategy built into ACIS.) Allocations and de-allocations of persistent variables are directed to the ACIS memory management system by using macros for the allocations and deallocations instead of the system functions and operators. Two of the more commonly used macros used to allocate and de-allocate memory are ACIS_NEW and ACIS_DELETE. These macros are used instead of the standard operator new and operator delete. In Tutorial 2 we mentioned that every source file that uses ACIS should include the acis.hxx header file. One of the benefits of including this header file is the definitions of all of the macros for memory allocation and de-allocation are included. Construction and Destruction of ENTITYs Most ENTITYs will be constructed for you by calling API and direct interface functions; however, occasionally you may need to construct an instance of a class derived from ENTITY in your application. In this case you will use the ACIS_NEW macro rather than the operator new. An example of this is shown below. SPAposition my_loc(0.0, 0.0, 0.0); APOINT * pt = ACIS_NEW APOINT(my_loc); VERTEX * vert = ACIS_NEW VERTEX(pt); Because ENTITYs are maintained by the ACIS history mechanism to facilitate roll back and roll forward (that is, undo and redo) you will typically never de-allocate the memory for an ENTITY; instead you will call either the lose method of the ENTITY or a higher level function that calls the lose method for you. The reason for calling a higher level function is because you often want to lose a set of connected ENTITYs. For example, you would not want to manually call lose for each of the topological, geometrical, and application-specific ENTITYs in a complex BODY. You would want to call a single function that traverses the data structure and calls lose on each of the ENTITYs for you. In addition, many types of ENTITYs are use counted, so you do not want to directly call lose on them. There are two higher level functions that are typically called to lose ENTITYs. API Function Description api_delent(ENTITY*,...) Deletes a topological ENTITY and all subentities. api_del_entity(ENTITY*,...) Deletes a topological ENTITY and all connected entities. The difference between these two functions is very significant. api_delent loses the given topological entity, any associated geometrical entity, any associated attributes on the given entity, and then traverses the topological structure downward, losing all lower level topological entities and their associated geometric and attributes. api_del_entity loses the given topological entity, any associated geometrical entity, any associated attributes on the given entity, and then traverses the data structure upward, downward, and horizontally, losing all connected entities. If entities in one BODY point to entities in another BODY, all entities in both BODIES will be lost. Neither function can modify pointers in your application that pointed to the lost entities. It is the responsibility of the application programmer to be sure that no dangling pointers remain. The use of api_delent and api_del_entity are demonstrated below. SPAposition my_loc(0.0, 0.0, 0.0); APOINT * my_pt = ACIS_NEW APOINT(my_loc); VERTEX * my_vert = ACIS_NEW VERTEX(my_pt); api_delent (my_vert); // Loses everything below my_vert my_vert = NULL; my_pt = NULL; BODY * my_block = NULL; api_make_cuboid (10.0, 10.0, 10.0, my_block); FACE * my_face = my_block->lump()->shell()->face(); api_del_entity (my_face); // Loses everything connected to my_face my_block = NULL; my_face = NULL; Construction and Destruction of non-ENTITY classes Instances of objects that are not derived from ENTITY should be constructed and destructed using the ACIS_NEW and ACIS_DELETE operators. This ensures consistent use of the underlying allocation and de-allocation functions. Use of ACIS_NEW and ACIS_DELETE to construct and destruct instances of classes not derived from ENTITY is demonstrated below. SPAposition * my_loc_ptr = ACIS_NEW SPAposition(10.0, 10.0, 10.0); ACIS_DELETE my_loc_ptr; Construction and Destruction of Standard Types The use of the ACIS_NEW macro when constructing a standard type object (for instance, a double or int) causes the memory allocation to be processed by the ACIS memory manager. When destructing an instance of a standard type that was constructed with ACIS_NEW one should always use the STD_CAST macro with ACIS_DELETE. This is demonstrated below. double * double_ptr = ACIS_NEW double; ACIS_DELETE STD_CAST double_ptr; Construction and Destruction of Arrays When destructing an array of objects one should always use [ ] with ACIS_DELETE just as with operator delete. This is demonstrated below. SPAposition * pos_array = ACIS_NEW SPAposition[NUM_LOCS]; ACIS_DELETE [] pos_array; int * i_array = ACIS_NEW int[NUM_INTS]; ACIS_DELETE [ ] STD_CAST i_array; Static Variables Because the ACIS memory management system has not been initialized when static objects are constructed, you should not create instances of ACIS classes as static variables in your application. Tutorial 9: Exception Handling Applications should be designed with exception handling in mind. In the event of an exception an application needs to be able to clean up allocated memory, repair data structures, and possibly try alternative solutions. ACIS provides two sets of macros to facilitate exception handling: the EXCEPTION set of macros and the API_BEGIN / API_END family of macros. The EXCEPTION Macros The EXCEPTION set of macros have been designed to appear similar to the C++ try/catch paradigm. These macros provide a foundation for writing exception-safe code. They are defined in errorsys.hxx. The basic structure of an ACIS EXCEPTION block is shown below. EXCEPTION_BEGIN // Declare variables that will be used during clean up. EXCEPTION_TRY // Perform geometric modeling operations. EXCEPTION_CATCH(FALSE) // Handle potential exceptions. EXCEPTION_END The EXCEPTION macros contain { and } so you do not need to add these. The above example appears to define three blocks of code between the four macros. Actually, there are just two blocks of code. The outer block is defined between EXCEPTION_BEGIN and EXCEPTION_END. The inner block is defined between EXCEPTION_TRY and EXCEPTION_CATCH. Therefore, variables that are declared between EXCEPTION_BEGIN and EXCEPTION_TRY are in scope between EXCEPTION_CATCH and EXCEPTION_END. Alternatively, variables that are declared within the EXCEPTION_TRY / EXCEPTION_CATCH block are out of scope after the EXCEPTION_CATCH statement. In addition, variables that are declared anywhere in the EXCEPTION block will be out of scope after the EXCEPTION_END; therefore, any variables that must be visible after the EXCEPTION block should be declared before the EXCEPTION block. A more specific example appears below. EXCEPTION_BEGIN SPAvector * vec_array = NULL; EXCEPTION_TRY //. //. //. vec_array = ACIS_NEW SPAvector[NUM_VECS]; //. //. //. EXCEPTION_CATCH(FALSE) // Process only if an exception occurs. ACIS_DELETE [ ] vec_array; EXCEPTION_END In the above example, vec_array is declared in the section after the EXCEPTION_BEGIN macro, memory for it is allocated in the section between the EXCEPTION_TRY and EXCEPTION_CATCH macros, and in the event of an exception the memory is de-allocated in the section after the EXCEPTION_CATCH macro. What if you wanted a fail-safe means to always clean up vec_array? The EXCEPTION_CATCH macro takes an argument that specifies whether the block should always be executed or executed only in the event of an exception. If this argument is FALSE, as in the previous example, then the block is executed only in the event of an exception. If this argument is TRUE, then the block is always executed. This is demonstrated below. EXCEPTION_BEGIN SPAvector * vec_array = NULL; EXCEPTION_TRY //. //. //. vec_array = ACIS_NEW SPAvector[NUM_VECS]; //. //. //. EXCEPTION_CATCH(TRUE) // Always perform clean up. ACIS_DELETE [ ] vec_array; EXCEPTION_END A logical question you might ask at this point is, "What should I do if some clean up is always required and some is required only in the event of an exception?" In this case, you can examine the variable error_no to determine if an exception occurred. If the value of error_no is non-zero, an exception has occurred. This is demonstrated below. EXCEPTION_BEGIN SPAvector * volatile vec_array1 = NULL; SPAvector * volatile vec_array2 = NULL; EXCEPTION_TRY //. //. //. vec_array1 = ACIS_NEW SPAvector[NUM_VEC1]; //. //. //. vec_array2 = ACIS_NEW SPAvector[NUM_VEC2]; //. //. //. EXCEPTION_CATCH(TRUE) // Always perform clean up. ACIS_DELETE [ ] vec_array1; if (error_no != 0) { // Process only if an exception occurs. if (vec_array2 != NULL) ACIS_DELETE [ ] vec_array2; } EXCEPTION_END The ACIS EXCEPTION macros are designed to be nested, but the only place exceptions should occur is during the EXCEPTION_TRY / EXCEPTION_CATCH block. In other words, you should design you code so that exceptions will not occur before the EXCEPTION_TRY macro or after the EXCEPTION_CATCH macro. The EXCEPTION_END macro automatically re-signals the error to next higher level EXCEPTION block. To prevent automatically passing control to the EXCEPTION_CATCH of the next higher level EXCEPTION block, you can set the value of resignal_no to 0. By default it is set to the value of error_no. Alternatively, you can use the EXCEPTION_END_NO_RESIGNAL macro. This is demonstrated in the example below in which foo1 calls foo2. If an exception occurs in foo2, it is propagated to foo1, but foo1 does not propagate the exception to its calling a function. void foo1() { EXCEPTION_BEGIN SPAvector * volatile vec_array1 = NULL; EXCEPTION_TRY //. //. //. vec_array1 = ACIS_NEW SPAvector[NUM_VEC1]; //. //. //. foo2(); //. //. //. EXCEPTION_CATCH(FALSE) // Process only if an exception occurs. ACIS_DELETE [ ] vec_array1; EXCEPTION_END_NO_RESIGNAL } void foo2() { EXCEPTION_BEGIN SPAvector * volatile vec_array2 = NULL; EXCEPTION_TRY //. //. //. vec_array2 = ACIS_NEW SPAvector[NUM_VEC2]; //. //. //. EXCEPTION_CATCH(FALSE) // Process only if an exception occurs. ACIS_DELETE [ ] vec_array2; EXCEPTION_END } Notice that in the event of an exception, all of the memory allocations that occurred in foo2 are cleaned up in foo2 and all of the memory allocations that occurred in foo1 are cleaned up in foo1. Also notice that in these last two examples, the variables in the EXCEPTION_BEGIN blocks are declared to be volatile. For information on the use of the volatile type qualifier, refer to HowTo:Use the volatile type qualifier. There are two EXCEPTION macros that we have not mentioned but may be of interest to you: EXCEPTION_CATCH_FALSE and EXCEPTION_CATCH_TRUE. The behavior of these macros is identical to EXCEPTION_CATCH(FALSE) and EXCEPTION_CATCH(TRUE). These macros were introduced because EXCEPTION_CATCH(FALSE) and EXCEPTION_CATCH(TRUE) cause compiler warnings on some platforms. Of course, EXCEPTION_CATCH_FALSE and EXCEPTION_CATCH_TRUE may cause compiler warnings on other platforms. The choice as to which to use is up to you. The complete set of EXCEPTION macros is summarized in the table below. Macro Description EXCEPTION_BEGIN start of the variable declaration section EXCEPTION_TRY start of the algorithmic section EXCEPTION_CATCH(AlwaysClean) start of the cleanup section processing depends on the value of AlwaysClean EXCEPTION_CATCH_FALSE Logically the same as EXCEPTION_CATCH(FALSE) EXCEPTION_CATCH_TRUE Logically the same as EXCEPTION_CATCH(TRUE) EXCEPTION_END end of the exception block resignal an exception if (resignal_no != 0) EXCEPTION_END_NO_RESIGNAL end of the exception block do not resignal an exception Important: You should not lose ENTITYs or attempt to correct the ACIS data structure using the EXCEPTION macros. The ACIS data structure should be cleaned up by rolling back the model to a previously valid state. This will be described below when we discuss the API_BEGIN/END macros. Note: We strongly recommend that your EXCEPTION blocks do not contain a return, goto, break, or other statement that would prematurely transfer control of out them. Aside: How to Generate Exceptions and Error Messages In Tutorials (Topology), we introduced sys_error and find_err_mess but did not explain much about their usage. sys_error is used to "throw" a typed exception that is intended to be caught by either an EXCEPTION block or an API_BEGIN / API_END block. sys_error has multiple signatures but we shall describe only the single argument signature of sys_error in this tutorial. This should be sufficient for most applications. sys_error(err_mess_type) takes a single argument, the error number. ACIS contains an Error Messaging system that maintains a list of error numbers and corresponding error messages. The Error Messaging system allows you to use, modify, and extend the lists of pre-defined error numbers and error messages. Actually, from the developer's perspective, ACIS maintains a list of symbols and corresponding error messages. At runtime, the symbols are given unique error numbers. In your application you can obtain the error message corresponding to a given symbol by using the function find_err_mess(err_mess_type) as we have demonstrated in previous tutorials. The addition and modification of ACIS error messages are described in Error Handling. For an exhaustive enumeration of all ACIS error symbols, their associated error message, and the file that contains them, you may refer to ACIS Error Messages. For the purposes of these tutorials, we shall either propagate an already thrown exception or use the generic symbol: API_FAILED. Important: Calls to sys_error generate an exception; therefore, all sys_error calls must be within an EXCEPTION block or a API_BEGIN / API_END block or else your application will abort when the exception is "thrown." Tutorial 10: History This tutorial provides an introduction to the ACIS history mechanism. ACIS provides some rather complex mechanisms for managing the history of a model; however, in this tutorial we shall concentrate on the basic concepts. In particular we shall focus on a single history stream and we will not use the part manager. If after reading this tutorial you desire to learn more about these more advanced topics, refer to History and Roll and Part Management. An Overview of the History Mechanism As we have mentioned in previous tutorials, all operations that change the ACIS model must be within an API_BEGIN/END block. (By this we mean all model changes must be within a block of code surrounded by a pair of macros in the API_BEGIN/API_END family.) As we mentioned before these blocks can be nested. The outermost API_BEGIN statement denotes the beginning of a bulletin board and the outermost API_END statement denotes the end of the bulletin board. Every creation, modification, or deletion of an ENTITY during the operation will be recorded on a bulletin. Thus, a bulletin board contains a list of bulletins, each containing a record of the creation, modification, or deletion of one ENTITY. Because one operation (from an end user's perspective) may consist of multiple API_BEGIN/END blocks (perhaps as the result of multiple calls to ACIS API functions), bulletin boards may be grouped together into a single delta state representing the entire operation. A delta state is simply an ordered list of bulletin boards. A modeling session may consist of many operations, so there is a need for a higher level concept that may contain many delta states. This is the role of the history stream. A history stream contains an n-ary tree structure of delta states. If the modeling session contains no rolled back operations, the tree structure will be linear, essentially a linked list. If the modeling session had some rolled back operations and these operations were not pruned from the tree structure, the tree structure will contain branches. In other words, every state of the ACIS model during a modeling session can be maintained in the history stream. Of course a modeling session could be quite complicated, requiring thousands or millions of ENTITY changes, which could result in a very large history stream, which would require a very large amount of memory. Therefore, ACIS provides mechanisms to reduce the number of delta states and bulletin boards retained in the model. The Data Structure Bulletins The lowest level object used in the history mechanism is the BULLETIN. (Although BULLETIN is in upper case, it is not derived from ENTITY. The same holds true for BULLETIN_BOARDs, DELTA_STATEs, and HISTORY_STREAMs.) A BULLETIN contains a record of an ENTITY being created, changed, or deleted. The BULLETIN contains a pointer to a copy of the ENTITY before the operation and a copy of the ENTITY after the operation. If the pointer to the "old" ENTITY (that is, the copy of the ENTITY before the operation) is NULL, then the BULLETIN is recording the creation of the ENTITY. The ENTITY did not exist before the operation. If the pointer to the "new" ENTITY (that is, the copy of the ENTITY after the operation) is NULL, then the BULLETIN is recording the deletion of the ENTITY. The ENTITY did not exist after the operation. If both pointers are non-NULL, implying the ENTITY existed before and after the operation, then the BULLETIN is recording a change to the ENTITY. A BULLETIN also contains pointers to the "next" and "previous" BULLETINs in a doubly linked list, and a pointer to the BULLETIN_BOARD that contains the doubly linked list of BULLETINs; that is, the "owner" of the BULLETIN. Bulletin Boards The next lowest level object used in the history mechanism is the BULLETIN_BOARD. A BULLETIN_BOARD contains a doubly linked list of BULLETINs. Specifically, the BULLETIN_BOARD contains a pointer to the first and last BULLETINs in the BULLETIN_BOARD. A BULLETIN_BOARD is the lowest level construct that can be independently rolled. If an exception occurs inside of an API_BEGIN/END block or an API_TRIAL_BEGIN/END block, the BULLETIN_BOARD associated with that operation will be rolled back. The BULLETIN_BOARD associated with an API_NOP_BEGIN/END block will always be rolled back. BULLETIN_BOARDs are maintained in a singly linked list; therefore, each BULLETIN_BOARD contains a "next" pointer. In addition, each BULLETIN_BOARD contains a pointer to the DELTA_STATE that contains the singly linked list of BULLETIN_BOARDs; that is, the "owner" of the BULLETIN_BOARD. Delta States Whereas a BULLETIN_BOARD is the lowest level construct that can be independently rolled, a DELTA_STATE is the construct with which the end user would interact to roll the model backward and forward, to perform "undo" and "redo" operations on the model. A DELTA_STATE contains one or more BULLETIN_BOARDs and typically represents an entire operation from the end user's perspective. As its name implies, a DELTA_STATE records the difference between two states, not a state. A DELTA_STATE contains a pointer to the first BULLETIN_BOARD in a singly linked list of BULLETIN_BOARDs. DELTA_STATEs are maintained in a tree structure. Each DELTA_STATE contains "next", "previous", and "partner" pointers. The "next" and "previous" pointers often seem reversed to new ACIS developers. "next" and "previous" are described with respect to rollback. The "partner" pointer is a "sibling" pointer in the n-ary tree structure. "partner" pointers allows branches in the history. If there were no "partner" pointers, the history would be a doubly linked list of DELTA_STATEs. Each DELTA_STATE also contains a pointer to the HISTORY_STREAM that contains the tree of DELTA_STATEs; that is, the "owner" of the DELTA_STATE. In addition, each DELTA_STATE contains a pointer to a character string that can be used to uniquely identify the DELTA_STATE. BULLETIN_BOARDs are associated with API_BEGIN/END blocks. DELTA_STATEs are associated with calls to api_note_state. Whenever api_note_state is called, all the BULLETIN_BOARDs created since the previous call to api_note_state are stored in a DELTA_STATE. In other words, a DELTA_STATE represents the model changes between calls to api_note_state. An application can roll a DELTA_STATE backward or forward. (In fact, below we will describe how an application can roll from one state to any another state.) If a DELTA_STATE has been rolled an odd number of times, its next roll will roll it forward. If a DELTA_STATE has been rolled an even number of times, its next roll will roll it backward. For example, if a DELTA_STATE has never been rolled, it will roll backward. A DELTA_STATE contains a flag that describes whether its next roll will be backward or forward. Some necessary terminology: During a modeling session there is often an "open" DELTA_STATE. This is the "current" DELTA_STATE. As BULLETIN_BOARDs are created they are added to the "current" DELTA_STATE. When api_note_state is called the "current" DELTA_STATE is "closed" and another DELTA_STATE is "opened." The newly opened DELTA_STATE becomes the "current" DELTA_STATE and the most recently closed DELTA_STATE becomes the "active" DELTA_STATE. The "active" DELTA_STATE is the DELTA_STATE that points to the current state of the model and records a backward change. Thus, when a DELTA_STATE is rolled the DELTA_STATE that was constructed immediately before the most recently rolled DELTA_STATE becomes the "active" DELTA_STATE. (For example, if there were three DELTA_STATEs and the third DELTA_STATE were rolled back, the second DELTA_STATE would become the "active" DELTA_STATE. If the third DELTA_STATE were rolled forward, it would again become the "active" DELTA_STATE.) The "current" DELTA_STATE is added directly after the "active" DELTA_STATE. In addition to the "current" and "active" DELTA_STATEs, the initial DELTA_STATE in the model is called the "root" DELTA_STATE. To permit the end user to move between specific states, the application must remember the various DELTA_STATEs. History Streams The HISTORY_STREAM is the highest level construct in the core ACIS history mechanism. A HISTORY_STREAM provides a pointer to the "current", "active", and "root" DELTA_STATEs. In this tutorial we shall use a single HISTORY_STREAM for the construction of our models. Such a history stream is sufficient for most applications. ACIS does allow applications to use multiple history streams, but that is beyond the scope of this tutorial. ACIS provides an API function, api_get_default_history, which returns a pointer to the default history stream. If an application uses multiple history streams it is up to the application to remember the various history streams. (The HISTORY_STREAM_LIST, HISTORY_MANAGER, and StreamFinder classes may be of interest if you choose to use multiple history streams.) Using the History Mechanism There are four things an ACIS application developer should understand how to do with the ACIS history mechanism. • Search a history stream to find specific BULLETINs or ENTITYs. • Control the granularity of BULLETIN_BOARDs and DELTA_STATEs for exception handling and rollback. • Roll to a specific state. • Remove unwanted DELTA_STATEs from a history stream. The ACIS history mechanism has many more capabilities than these, but these are the ones used most frequently by applications. Searching a History Stream With a single history stream, you can obtain the HISTORY_STREAM using api_get_default_history. HISTORY_STREAM * hs = NULL; outcome result = api_get_default_history(hs); The "root," "current," and "active" DELTA_STATEs can be obtained from a HISTORY_STREAM using member functions. DELTA_STATE * root_ds = hs->get_root_ds( ); DELTA_STATE * current_ds = hs->get_current_ds( ); DELTA_STATE * active_ds = hs->get_active_ds( ); DELTA_STATEs within a single HISTORY_STREAM can be traversed using their "previous," "next," and "partner" pointers. DELTA_STATE * prev_ds = ds->prev( ); DELTA_STATE * next_ds = ds->next( ); DELTA_STATE * partner_ds = ds->partner( ); The BULLETIN_BOARD pointer of a DELTA_STATE can be obtained using the member function: DELTA_STATE::bb. BULLETIN_BOARD * bb = ds->bb( ); The currently open BULLETIN_BOARD can be obtained by the global function, current_bb. BULLETIN_BOARD * curr_bb = current_bb( ); BULLETIN_BOARDs within a DELTA_STATE can be traversed using their "next" pointers. BULLETIN_BOARD * next_bb = bb->next( ); The first or last BULLETIN pointer of a BULLETIN_BOARD can be obtained using the member functions: BULLETIN_BOARD::start_bulletin or BULLETIN_BOARD::end_bulletin. BULLETIN * start_b = bb->start_bulletin( ); BULLETIN * end_b = bb->end_bulletin( ); BULLETINs within a BULLETIN_BOARD can be traversed using their "previous" and "next" pointers. BULLETIN * prev_b = b->previous( ); BULLETIN * next_b = b->next( ); Three BULLETIN member functions are very useful for examining bulletins: new_entity_ptr, old_entity_pointer and type. ENTITY * new_ent = b->new_entity_ptr(); ENTITY * old_ent = b->old_entity_ptr(); BULLETIN_TYPE b_type = b->type(); A BULLETIN_TYPE is an enumeration with four values: NO_BULLETIN, CREATE_BULLETIN, CHANGE_BULLETIN, and DELETE_BULLETIN. You should never see a NO_BULLETIN when traversing a bulletin board. In a CREATE_BULLETIN the old entity will always be NULL. In a DELETE_BULLETIN the new entity will always be NULL. Every ENTITY also contains a pointer to its most recent BULLETIN. This may be obtained using the member function, rollback. BULLETIN * b = ent->rollback( ); The first example program demonstrates how the above functions can be combined to interrogate a linear history stream to investigate what happened during a series of operations. Why would an application need to examine a history stream? There are many reasons. One of the most common is that an application will create a data structure that is connected to or parallels the ACIS data structure and the application examines the changes to the ACIS model after each operation to see what changed in the ACIS model. An application may also want to pause mid-operation to obtain information about changes to the model that are contained within a specific bulletin board. By controlling the granularity of bulletin boards one can make such interrogations easier. Controlling the Granularity of Bulletin Boards and Delta States If there are nested API_BEGIN/END blocks, bulletin boards are constructed for the outer most API_BEGIN/END block. If an API_BEGIN/END block is not nested, a bulletin board is constructed just for that block of code. In addition to your API_BEGIN/END blocks, each ACIS API function that changes the model will construct a single bulletin board. If you want several ACIS API functions or several of your API_BEGIN/END blocks to be in the same bulletin board, you can surround them with an API_BEGIN/END block. (This applies to API_TRIAL_BEGIN/END and API_NOP_BEGIN/END blocks, too.) ACIS contains an option, compress_bb, that controls the compression (or merging) of bulletin boards on a delta state. If compress_bb is FALSE, the bulletin boards are not merged. If compress_bb is TRUE, a bulletin board will be merged with the previous bulletin board on the same delta state, if there is one and if the outcome of the bulletin board is successful. This occurs when the second bulletin board is closed on a delta state. If the outcome of any bulletin board is not successful, the model will be automatically rolled back to the state it was in at the API_BEGIN statement. What is the difference between (a) creating a single bulletin board within a delta state and (b) creating multiple bulletin boards within a delta state that automatically get merged into one bulletin board? The difference is what happens when an exception occurs. If an operation is successful, there is no difference. If a failure occurs, only the unsuccessful bulletin boards is rolled back. This may allow an end user to get partial results from an operation, or it may allow an application to attempt multiple algorithms to complete an operation. As we stated above a delta state is created when you call api_note_state – if model changes have occurred. If no model changes have occurred, api_note_state will return a pointer to an empty delta state. So, the granularity of delta states depends on the placement of calls to api_note_state. The granularity of delta states dictates the granularity of rollback functionality exposed to the end user, because programmatic rollback occurs at the delta state level. Some compression and merging can occur after the fact. Multiple bulletin boards within a delta state can be merged after the delta states have been closed by calling DELTA_STATE::compress. Multiple delta states can be merged by calling api_merge_states. Merging delta states can reduce the size of a model, but it is also time consuming and restricts the granularity of rollback. Rolling to a Specific State A delta state captures the changes to the ACIS model that occurred between two states. Let's call them Staten-1 and Staten. If the model is at Staten it can be change to the Staten-1 by rolling back the delta state. If the model is at Staten-1 it can be change to the Staten by rolling the delta state forward. Rolling a delta state backward or forward is achieved by calling api_change_state(DELTA_STATE*). The direction of rolling depends on the current state of the model (in particular, the state of the delta state.) An application can roll between states separated by multiple delta states by calling api_change_state(DELTA_STATE*) repeatedly, using the appropriate delta states. Alternatively, an application can call api_change_to_state(HISTORY_STREAM*,DELTA_STATE*) to change directly to the state of the model when api_note_state was called for the given DELTA_STATE. Another alternative is for the application to call api_roll_n_states(HISTORY_STREAM*, int n,...), which will roll the model forward or backward 'n' states, depending on whether 'n' is positive or negative. We should mention that typically one rolls among the various states of the current session; however, it is possible to save an ACIS model including its history, so one could restore a model and roll it back to a state in a previous session. To provide this functionality the application should save the model and its history with api_save_entity_list_with_history and restore it with api_restore_entity_list_with_history, instead of saving the model with api_save_entity_list and restoring it with api_restore_entity_list. Removing Unwanted Delta States If a model has been rolled back and a set of delta states is no longer needed, the unneeded delta states can be deleted. Two relatively high level API functions provide functionality to accomplish this. api_prune_following prunes away all the delta states after the active delta state. (Remember the active delta state is the one before the one most recently rolled.) api_prune_history can be used to prune away specific portions of the history stream before or after the active delta state. Thus, in addition to removing future states that are no longer needed, an application can reduce the number of past states that are maintained in the history stream. Alternatively, the number of past states maintained by the history stream can be limited by calling HISTORY_STREAM::set_max_states_to_keep, which will cause one previous delta state to be pruned away as each new delta state is added to the history stream. For the creation of branched history streams you should be familiar with the delete_forward_states option. This option controls whether or not branches are created. When the delete_forward_states option is TRUE, the default, delta states forward of the active delta state are deleted when a new delta state is added to the history stream. If you should choose to use branched history streams, you should be aware of HISTORY_STREAM::prune_inactive_branch(DELTA_STATE*), which will prune away all of the partners of the specified DELTA_STATE. Two Programming Notes One thing you should be aware of is how bulletins are created. Bulletins are created by a call to ENTITY::backup. This member function causes a bulletin to be constructed and added to the current bulletin board, if one is already open. As you recall, bulletin boards are constructed and opened by API_BEGIN, API_NOP_BEGIN, and API_TRIAL_BEGIN. If you attempt to construct a bulletin and add it to a non-existent bulletin board, an error will occur. Attempting to modify an ENTITY without first having called backup will cause significant problems, although not necessarily an immediate error. Calling backup more than once for an ENTITY does not cause a problem. All ACIS API functions and direct interface functions will call ENTITY::backup for you. If you create new attribute or entity classes, you will need to call backup in your member data setting functions. (This is described in the next tutorial, Tutorials (Attributes).) You should also be aware that some query functions may cause changes to the model, so they must call backup, so they must be called within API_BEGIN/END blocks. For instance, obtaining the bounding box for a topological entity may cause bounding boxes to be calculated and cached on entities. Many queried quantities are now cached on entities (to enhance performance) which can lead to unanticipated model changes. For this reason new developers may want all direct interface calls that operate on ENTITYs to be within API_BEGIN/END blocks. A Question We have said that API_BEGIN/END blocks, API_TRIAL_BEGIN/END blocks, and API_NOP_BEGIN/END blocks create bulletin boards... so, how can the ACIS API functions mentioned above (such as, api_change_state) manipulate the history stream without generating unwanted bulletin boards? . . . In Tutorials (Exception Handling) we mentioned that there are four sets of macros in the API_BEGIN / API_END macro family. We did not describe the API_SYS_BEGIN and API_SYS_END macros because they are not for use by application developers. These macros do not generate bulletin boards; therefore, they are used to manipulate the history-related data structures. C++ Example The Program We want to create a simple model using multiple operations so we can explore what happens within the history stream as the model is constructed. For the model we have chosen to use multiple copies of the sheet body created in the Geometry Tutorial. The construction of each sheet body will be considered to be a single operation. After the construction of each sheet body we call api_note_state thereby creating a delta state containing the bulletins and bulletin boards for the construction of that sheet body. The construction of each sheet body is performed by my_create_sheet_body. The construction of the entire model is performed by my_generate_model. For this example we construct three sheet bodies. The image below depicts the model generated in this example. Three Sheet Bodies After generating the model we examine the contents of the history stream using my_debug_history_stream. my_debug_history_stream traverses through the delta states in the history stream, calling my_debug_delta_state for each one. The amount of debug information produced by my_debug_delta_state is controlled by the debug_level argument. • If debug_level is set to 0, we do not examine the bulletins. • If debug_level is set to 1, we examine the type of each bulletin and each ENTITY on each bulletin. We record creations, changes, and deletions of faces, edges, and coedges. (You can easily expand the types of ENTITYs recorded.) • If debug_level is set to 2, we print out the type of bulletin and ENTITY for every bulletin. This example demonstrates how one can temporarily set the compress_bb option to FALSE so that each delta state will contain multiple bulletin boards. my_create_sheet_body demonstrates the use of an API_BEGIN/END block to combine multiple API functions on a single bulletin board. my_create_sheet_body generates three bulletin boards despite calling four API functions. If the compress_bb option were set to TRUE (the default value) the bulletin boards on each delta state would be merged together, creating just one bulletin board on each delta state. This example also demonstrates how the ACIS EXCEPTION macros can be used outside of API_BEGIN/END blocks to provide exception handling capabilities where you do not want to affect the construction of bulletin boards. In fact, we could not use an API_BEGIN/END block in do_something because api_note_state is called repeatedly in my_generate_model. C++ Example #include <stdio.h> #include "acis.hxx" #include "api.hxx" #include "api.err" #include "kernapi.hxx" #include "cstrapi.hxx" #include "coverapi.hxx" #include "boolapi.hxx" #include "lists.hxx" #include "alltop.hxx" #include "get_top.hxx" #include "bulletin.hxx" // Declaration of the ACIS licensing function. void unlock_spatial_products_<NNN>(); // Declaration of our functions. void do_something(); outcome my_generate_model(int, ENTITY_LIST&); outcome my_create_sheet_body(double, BODY*&); void my_debug_history_stream(); void my_debug_delta_state(DELTA_STATE*,int); int my_initialization(); int my_termination(); // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and propagate the error. #define CHECK_RESULT \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ sys_error(err_no); \ } // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and return the outcome. #define CHECK_RESULT2 \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ return result; \ } // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and return the error number. #define CHECK_RESULT3 \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ return err_no; \ } // The main program... int main (int argc, char** argv) { int ret_val = my_initialization(); if (ret_val) return 1; do_something(); ret_val = my_termination(); if (ret_val) return 1; printf("Program completed successfully\n\n"); return 0; } void do_something(){ // Generate a number of bodies on separate Delta States // to simulate an ACIS modeling session. Then examine // the Delta States to see what they contain. EXCEPTION_BEGIN option_header * compress_bb_option = NULL; ENTITY_LIST bodies; EXCEPTION_TRY // Turn off Bulletin Board compression compress_bb_option = find_option("compress_bb"); compress_bb_option->push(FALSE); // Generate some bodies int num_bodies = 3; outcome result = my_generate_model(num_bodies, bodies); CHECK_RESULT // Examine the History Stream to see what was captured. my_debug_history_stream(); EXCEPTION_CATCH(TRUE) // Before exiting this function we should reset the option // and delete the bodies we created. compress_bb_option->pop(); for (int i = 0; i < bodies.count(); i++) api_delent(bodies[i]); EXCEPTION_END_NO_RESIGNAL return; } outcome my_generate_model( int num_bodies, // in: number of bodies to create ENTITY_LIST & bodies // out: list of bodies created ) { // This function simulates an ACIS modeling session. // It creates a number of bodies, each on a separate Delta State. outcome result; for (int i = 0; i < num_bodies; i++) { // Create a sheet body consisting of two faces. BODY * my_body = NULL; result = my_create_sheet_body((double)i, my_body); CHECK_RESULT2 // Append the sheet body to the list of bodies bodies.add(my_body); // Note the state of the model DELTA_STATE * ds; result = api_note_state(ds); CHECK_RESULT2 } return result; } outcome my_create_sheet_body( double z_coord, // in: z coordinate of the body's mid-point BODY *& my_body // out: created body ) { // This function creates a sheet body consisting of two // double-sided faces. It calls four API functions. // We shall surround the first two API functions with // API_BEGIN / API_END macros to combine these 2 APIs // into one Bulletin Board. FACE * my_face = NULL; // The single-sided face created by // the the API_BEGIN / API_END block. API_BEGIN // Create a polygonal wire body from an array of positions. // The first point is the same as the last to create a closed wire. // The wire body will contain 4 edges and 4 vertices. SPAposition * pos_array = ACIS_NEW SPAposition[5]; pos_array[0] = SPAposition(-5.0, -5.0, z_coord - 1.0); pos_array[1] = SPAposition(-5.0, 5.0, z_coord + 1.0); pos_array[2] = SPAposition( 5.0, 5.0, z_coord - 1.0); pos_array[3] = SPAposition( 5.0, -5.0, z_coord + 1.0); pos_array[4] = SPAposition(-5.0, -5.0, z_coord - 1.0); result = api_make_wire (my_body, 5, pos_array, my_body); CHECK_RESULT ACIS_DELETE [] pos_array; // De-allocate the array. // Convert the wire body into a solid body. // This is accomplished by covering the wire with a face. // The solid body will contain one single-sided face. WIRE * my_wire = my_body->lump()->shell()->wire(); result = api_cover_wire (my_wire, *(surface*)NULL_REF, my_face); CHECK_RESULT API_END if (!result.ok()) return result; // Convert the solid body into a sheet body. // The sheet body will contain one double-sided face. result = api_body_to_2d (my_body); CHECK_RESULT2 // Split the face into two faces along an isoparametric curve. // We will perform the split in the u-direction at the mid // parameter of the surface. result = api_split_face (my_face, TRUE, TRUE, 0.5); CHECK_RESULT2 return result; } void my_debug_history_stream() { // This function debugs the default History Stream. HISTORY_STREAM * hs = NULL; outcome result = api_get_default_history(hs); if (!result.ok()) return; printf ("\n******************************************************\n"); DELTA_STATE * root_ds = hs->get_root_ds(); printf ("The root Delta State is 0x%x\n", (long)root_ds); DELTA_STATE * current_ds = hs->get_current_ds(); printf ("The current Delta State is 0x%x\n", (long)current_ds); DELTA_STATE * active_ds = hs->get_active_ds(); printf ("The active Delta State is 0x%x\n\n", (long)active_ds); // Set the amount of debug information desired const int debug_level = 1; // Examine the Active Delta State and // follow the next pointers from it. // (The next pointers should point backward.) if (active_ds) { printf ("Details of the active Delta State\n\n"); my_debug_delta_state(active_ds, debug_level); // Follow the next Delta State pointers DELTA_STATE * this_ds = active_ds->next(); if (this_ds) printf ("Scanning the next Delta State pointers\n\n"); while (this_ds) { my_debug_delta_state(this_ds, debug_level); this_ds = this_ds->next(); } // Examine the Active Delta State's partner and // follow the next pointers from it. // (The next pointers should point forward.) if (active_ds != active_ds->partner()) { DELTA_STATE * partner_ds = active_ds->partner(); printf ("Details of the active Delta State's partner\n\n"); my_debug_delta_state(partner_ds, debug_level); // Follow the next Delta State pointers this_ds = partner_ds->next(); if (this_ds) printf ("Scanning the next Delta State pointers\n\n"); while (this_ds) { my_debug_delta_state(this_ds, debug_level); this_ds = this_ds->next(); } } } } void my_debug_delta_state( DELTA_STATE * ds, // in: The Delta State to debug int debug_level // in: Amount of debug information // 0 : No Bulletin info // 1 : Summary Bulletin info // 2 : Detailed Bulletin info ) { // This function prints information about a given Delta State. // If brief is TRUE, it prints information only about the Delta State. // If brief is FALSE, it prints additional information about the // Bulletin Boards and Bulletins within the Delta State. printf("Delta State 0x%x\n", (long)ds); printf("Previous Delta State 0x%x\n", (long)ds->prev()); printf("Next Delta State 0x%x\n", (long)ds->next()); printf("Partner Delta State 0x%x\n", (long)ds->partner()); printf("Rolls %s ", ds->backward() ? "backward" : "forward"); printf("to state %d ", (int)ds->to()); printf("from state %d\n", (int)ds->from()); BULLETIN_BOARD * bb = ds->bb(); if (bb == NULL) printf("\tContains No Bulletin Boards\n"); while (bb) { printf("\tBulletin Board 0x%x\n", (long)bb); if (debug_level) { BULLETIN * b = bb->start_bulletin(); // Counters for summary debugging. // We could have many more, // but this should demonstrate the concept. int created_faces = 0; int changed_faces = 0; int deleted_faces = 0; int created_edges = 0; int changed_edges = 0; int deleted_edges = 0; int created_coedges = 0; int changed_coedges = 0; int deleted_coedges = 0; while (b) { ENTITY * old_ent = b->old_entity_ptr(); ENTITY * new_ent = b->new_entity_ptr(); BULLETIN_TYPE b_type = b->type(); if (debug_level == 1) { switch (b_type) { case CREATE_BULLETIN: if (is_FACE(new_ent)) created_faces++; else if (is_EDGE(new_ent)) created_edges++; else if (is_COEDGE(new_ent)) created_coedges++; break; case CHANGE_BULLETIN: if (is_FACE(new_ent)) changed_faces++; else if (is_EDGE(new_ent)) changed_edges++; else if (is_COEDGE(new_ent)) changed_coedges++; break; case DELETE_BULLETIN: if (is_FACE(old_ent)) deleted_faces++; else if (is_EDGE(old_ent)) deleted_edges++; else if (is_COEDGE(old_ent)) deleted_coedges++; break; } } else if (debug_level == 2) { const char * ent_type_str = (old_ent) ? old_ent->type_name() : (new_ent) ? new_ent->type_name() : "unknown"; const char * b_type_str = (b_type == CREATE_BULLETIN) ? "CREATE" : (b_type == DELETE_BULLETIN) ? "DELETE" : "CHANGE"; printf("\t\tBulletin 0x%x\n", (long)b); printf("\t\t%s %s, old_ent = 0x%x, new_ent = 0x%x\n", b_type_str, ent_type_str, (long)old_ent, (long)new_ent); } b = b->next(); } if (debug_level == 1) { printf("\t\tCreated %d faces %d edges and %d coedges\n", created_faces, created_edges, created_coedges); printf("\t\tChanged %d faces %d edges and %d coedges\n", changed_faces, changed_edges, changed_coedges); printf("\t\tDeleted %d faces %d edges and %d coedges\n", deleted_faces, deleted_edges, deleted_coedges); } } bb = bb->next(); } printf("\n"); } int my_initialization() { // Start ACIS. outcome result = api_start_modeller(0); CHECK_RESULT3 // Call the licensing function to unlock ACIS. unlock_spatial_products_<NNN>(); // Initialize all necessary components. result = api_initialize_kernel(); CHECK_RESULT3 return 0; } int my_termination() { // Terminate all necessary components. outcome result = api_terminate_kernel(); CHECK_RESULT3 // Stop ACIS and release any allocated memory. result = api_stop_modeller(); CHECK_RESULT3 return 0; } The output of the program should look similar to the following. Program Output  ****************************************************** The root Delta State is 0x3ab0c8 The current Delta State is 0x0 The active Delta State is 0x28bbfa0 Details of the active Delta State Delta State 0x28bbfa0 Previous Delta State 0x0 Next Delta State 0x28b42c8 Partner Delta State 0x28bbfa0 Rolls backward to state 4 from state 5 Bulletin Board 0x28c3c00 Created 1 faces 3 edges and 4 coedges Changed 1 faces 4 edges and 4 coedges Deleted 0 faces 0 edges and 0 coedges Bulletin Board 0x28c0928 Created 0 faces 0 edges and 0 coedges Changed 1 faces 0 edges and 0 coedges Deleted 0 faces 0 edges and 0 coedges Bulletin Board 0x28beac8 Created 1 faces 4 edges and 4 coedges Changed 0 faces 0 edges and 0 coedges Deleted 0 faces 0 edges and 0 coedges Scanning the next Delta State pointers Delta State 0x28b42c8 Previous Delta State 0x28bbfa0 Next Delta State 0x3ab138 Partner Delta State 0x28b42c8 Rolls backward to state 3 from state 4 Bulletin Board 0x28b8090 Created 1 faces 3 edges and 4 coedges Changed 1 faces 4 edges and 4 coedges Deleted 0 faces 0 edges and 0 coedges Bulletin Board 0x28b8e58 Created 0 faces 0 edges and 0 coedges Changed 1 faces 0 edges and 0 coedges Deleted 0 faces 0 edges and 0 coedges Bulletin Board 0x28ace08 Created 1 faces 4 edges and 4 coedges Changed 0 faces 0 edges and 0 coedges Deleted 0 faces 0 edges and 0 coedges Delta State 0x3ab138 Previous Delta State 0x28b42c8 Next Delta State 0x3ab0c8 Partner Delta State 0x3ab138 Rolls backward to state 2 from state 3 Bulletin Board 0x28b0c58 Created 1 faces 3 edges and 4 coedges Changed 1 faces 4 edges and 4 coedges Deleted 0 faces 0 edges and 0 coedges Bulletin Board 0x3abb20 Created 0 faces 0 edges and 0 coedges Changed 1 faces 0 edges and 0 coedges Deleted 0 faces 0 edges and 0 coedges Bulletin Board 0x3ab1a8 Created 1 faces 4 edges and 4 coedges Changed 0 faces 0 edges and 0 coedges Deleted 0 faces 0 edges and 0 coedges Delta State 0x3ab0c8 Previous Delta State 0x3ab138 Next Delta State 0x0 Partner Delta State 0x3ab0c8 Rolls backward to state 1 from state 2 Contains No Bulletin Boards Program completed successfully  Executing Variations of the Program Upon getting the example program to run successfully we would suggest running the program and capturing the output for six different cases. Run the program with values of 0, 1, and 2 for the debug_level. Then comment out the lines in do_something to push and pop the compress_bb option value. Then rerun the program with 0, 1, and 2 for the debug level. Before reading our discussion below study the various output files. What do you observe in the various output files? Discussion The six output files illustrate quite a few concepts about ACIS behavior. When you compare the two output files created with debug_level set to 0, you should immediately notice that when the compress_bb option set to FALSE each delta state contains three bulletin boards; whereas the default behavior (with the compress_bb option set to TRUE) there is only one bulletin board in each delta state. The three bulletin boards have been merged into one bulletin board. Merging bulletin boards on a delta state does take some time, but it reduces the size of the history stream (which also makes SAT and SAB files saved with history smaller) and it makes subsequent rolling between states more efficient. Not merging bulletin boards saves a little time during operation and allows applications to examine the contents of each bulletin board independently. In addition to compressing the bulletin boards on each delta state as it is constructed, it is possible to compress the bulletin boards on a delta state after it has been closed using DELTA_STATE::compress. When you compare the two output files created with debug_level set to 1, you should notice that there are no change bulletins in the history stream when the compress_bb option was TRUE. How is this possible? The create and change bulletins for a given entity have been merged into a single create bulletin. A bulletin board contains at most one bulletin for each entity. If two bulletin boards are being merged and there are bulletins for an entity on each bulletin board, the bulletins must be merged. A create and a change bulletin for an entity can be merged into a single create bulletin. Two change bulletins can be merged into one change bulletin. A change and a delete bulletin can be merged into a delete bulletin. And a create and a delete bulletin cancel each other out, meaning no record is kept if an entity is created and deleted in a single bulletin board. (This is an important fact to remember when searching through a history stream, looking for an entity that you were sure was created.) When you look at the order of the bulletin boards on the various delta states with debug_level set to 1, what do you notice? (The three bulletin boards correspond to (1) generating a single-side sheet body, (2) converting the single sided face to a double-sided face, and (3) splitting the double-sided face.) The bulletin boards are in the reverse order of the order in which they were created. The most recently closed bulletin board is first in the delta state. You might also notice that if you read the output file from the bottom up, it represents the order in which the operations occurred. The reason that bulletin boards are in the reverse order is for rollback. To undo an operation the bulletin boards on a delta state are undone in the reverse order to which they were created. The most recent bulletin board is undone first. Comparing the output with debug_level set to 2 can be a bit overwhelming. It is probably easiest just to look at one operation in each file. When looking at a delta state with compress_bb set to TRUE you should see there were only create bulletins and how many different types of entities were created. Looking at a delta state with compress_bb set to FALSE shows how much happened to the model during my_create_sheet_body. Let's look at one entity in one operation: the face that is created, made double-sided, and then split. If you can find the bulletin in the bulletin board that corresponds to the creation of the face, find the address of the face and search for all occurrences of this address in the file. You should find that the address of the face shows up three times in the file. The bulletins should look something like the following:  Bulletin 0x28af928 CHANGE face, old_ent = 0x28af8c0, new_ent = 0x3ab200   Bulletin 0x28acb48 CHANGE face, old_ent = 0x3ab3f0, new_ent = 0x3ab200   Bulletin 0x3ab2b0 CREATE face, old_ent = 0x0, new_ent = 0x3ab200  It may not be immediately obvious why the "new entity" in each case has the same address. Why do you think this is? Imagine for a moment that this represented three different operations instead of one operation. After the first operation your application captured a pointer to the face. After the second operation you would want the address to remain the same so your pointer would still be valid. Therefore, the "new entity" must have the same address as the entity did before the operation, which implies the copied entity is the "old entity," not the "new entity," in a change bulletin. If you were to run this program in a debugger using debug_level 1 you could set a breakpoint in my_debug_delta_state where we were looking for a CHANGE_BULLETIN for a FACE. If you were to run this program with this breakpoint set, you could compare the "old entity" and the "new entity." Note: For those of you who are running debuggers on non-Windows platforms, there are a number of functions declared in sg_debug.hxx that may be useful for debugging ACIS applications. Calling dbent(ENTITY const *) from the debugger will print the debug output for an entity. Calling dbhelp produces a list of the functions you can call. Examining the two entities on a bulletin can be a bit confusing the first time you do it. You must remember that the bulletin boards on a delta state are in reversed order. You must also remember that the "new entity" pointer on each bulletin is the address of the current entity; therefore, it cannot represent the "after" state of each sub-operation. In addition, the "old entity" may contain addresses which are the current addresses of entities. However, despite these difficulties, you can compare the "old entities," and compare the final "old entity" with the "new entity." Looking at a FACE with a CHANGE_BULLETIN in this example, you can see that initially there was no FACE. (The old_entity pointer was NULL on its CREATE_BULLETIN.) On the previous bulletin board (representing the next stage of the operation) the old FACE was single-sided and its containment bit was set (but meaningless.) This represents the FACE as it was constructed. On the previous bulletin board (representing the next stage of the operation) the old FACE was double-sided and its containment bit was set to 0 (meaning the containment was BOTH_OUTSIDE). Comparing the final old FACE with the current FACE, you can see that the loop pointer has changed. This occurred when the FACE was split. If we had examined the bulletins of the current bulletin board immediately after the API_END statement and each API function call in my_create_sheet_body, it would have been easier to determine the changes that had occurred in the model because the each new_entity pointer would have been current. Could you create a list containing all of the faces that were created during an operation that exists on one delta state? Could you create a list containing all of the bodies that currently exist in the model? Modifying the Example The Modified Program In the second example we shall modify our previous program slightly to examine the effects of rolling between states. We shall make two sets of changes. 1. Because we are rolling the model, we cannot trust the model to contain all of the bodies constructed by my_generate model. Therefore, we will create a new function, my_find_active_bodies that will scan the history stream and find all the bodies that still exist in the model. We will delete these bodies, rather than the bodies returned by my_generate model, before exiting the program. Typically an ACIS application will keep track of active top level entities for the end user, but our demo application does not, and this provides another opportunity to show how you can search the history stream. Because a body could be constructed on one bulletin board and deleted on another, my_find_active_bodies searches all of the bulletin boards for constructed and deleted bodies and returns only those bodies that were constructed and not deleted. Actually, my_find_active_bodies searches only the bulletin boards on delta states that have not been rolled back. These are the delta states between the root and active delta states. 2. The other set of changes will be to do_something. do_something will be enhanced to allow you to more easily generate the initial model, roll backward and forward, generate additional bodies, turn on and off bulletin board compression, and turn on and off branched history. These capabilities will be controlled by logical variables so you do not have to comment out code to see the effects of various changes. The only functions that will be changed are do_something, my_generate_model (which no longer returns an ENTITY_LIST containing the newly created bodies), and my_find_active_bodies. The other functions will be unchanged. Below is the revised program. Modified C++ Example #include <stdio.h> #include "acis.hxx" #include "api.hxx" #include "api.err" #include "kernapi.hxx" #include "cstrapi.hxx" #include "coverapi.hxx" #include "boolapi.hxx" #include "lists.hxx" #include "alltop.hxx" #include "get_top.hxx" #include "bulletin.hxx" // Declaration of the ACIS licensing function. void unlock_spatial_products_<NNN>(); // Declaration of our functions. void do_something(); outcome my_generate_model(int); outcome my_create_sheet_body(double, BODY*&); void my_debug_history_stream(); void my_debug_delta_state(DELTA_STATE*,int); void my_find_active_bodies(ENTITY_LIST&); int my_initialization(); int my_termination(); // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and propagate the error. #define CHECK_RESULT \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ sys_error(err_no); \ } // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and return the outcome. #define CHECK_RESULT2 \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ return result; \ } // Define a macro to check the outcome of API functions. // In the event of an error this macro will // print an error message and return the error number. #define CHECK_RESULT3 \ if (!result.ok()) { \ err_mess_type err_no = result.error_number(); \ printf("ACIS ERROR %d: %s\n", \ err_no, find_err_mess(err_no)); \ return err_no; \ } // The main program... int main (int argc, char** argv) { int ret_val = my_initialization(); if (ret_val) return 1; do_something(); ret_val = my_termination(); if (ret_val) return 1; printf("Program completed successfully\n\n"); return 0; } void do_something(){ // Generate a number of bodies on separate Delta States, // then optionally roll backward, roll forward, and generate // more bodies, to simulate an ACIS modeling session. Then // examine the History Stream to see what it contains. logical do_bb_compression = TRUE; logical allow_branched_history = FALSE; logical do_roll_backward = TRUE; logical do_roll_forward = FALSE; logical make_additional_bodies = FALSE; logical use_our_debugging = TRUE; logical use_acis_debugging = FALSE; EXCEPTION_BEGIN option_header * compress_bb_option = NULL; option_header * delete_forward_states_option = NULL; EXCEPTION_TRY // Turn off Bulletin Board compression if (do_bb_compression) { compress_bb_option = find_option("compress_bb"); compress_bb_option->push(FALSE); } // Allow Branched History Streams if (allow_branched_history) { delete_forward_states_option = find_option("delete_forward_states"); delete_forward_states_option->push(FALSE); } // Generate some initial bodies int num_bodies = 3; outcome result = my_generate_model(num_bodies); CHECK_RESULT // Alter the model and examine the History Stream HISTORY_STREAM * hs = NULL; result = api_get_default_history(hs); CHECK_RESULT if (do_roll_backward) { int n_actual; result = api_roll_n_states(hs, -2, n_actual); printf ("Actual number of states rolled = %d\n", n_actual); CHECK_RESULT } if (do_roll_forward) { int n_actual; result = api_roll_n_states(hs, 2, n_actual); printf ("Actual number of states rolled = %d\n", n_actual); CHECK_RESULT } if (make_additional_bodies) { num_bodies = 2; result = my_generate_model(num_bodies); CHECK_RESULT } // Note: Our History Stream debugging function // does not support branched History Streams. if (use_our_debugging) my_debug_history_stream(); // Note: For branched History Streams use HISTORY_STREAM::debug(). if (use_acis_debugging) hs->debug(0, 1, 1, stdout); EXCEPTION_CATCH(TRUE) // Before exiting this function we should reset the options // and delete all of the bodies we created. if (do_bb_compression) compress_bb_option->pop(); if (allow_branched_history) delete_forward_states_option->pop(); ENTITY_LIST bodies; my_find_active_bodies(bodies); for (int i = 0; i < bodies.count(); i++) api_delent(bodies[i]); EXCEPTION_END_NO_RESIGNAL return; } outcome my_generate_model( int num_bodies // in: number of bodies to create ) { // This function simulates an ACIS modeling session. // It creates a number of bodies, each on a separate Delta State. outcome result; for (int i = 0; i < num_bodies; i++) { // Create a sheet body consisting of two faces. BODY * my_body = NULL; result = my_create_sheet_body((double)i, my_body); CHECK_RESULT2 // Note the state of the model DELTA_STATE * ds; result = api_note_state(ds); CHECK_RESULT2 } return result; } outcome my_create_sheet_body( double z_coord, // in: z coordinate of the body's mid-point BODY *& my_body // out: created body ) { // This function creates a sheet body consisting of two // double-sided faces. It calls four API functions. // We shall surround the first two API functions with // API_BEGIN / API_END macros to combine these 2 APIs // into one Bulletin Board. FACE * my_face = NULL; // The single-sided face created by // the the API_BEGIN / API_END block. API_BEGIN // Create a polygonal wire body from an array of positions. // The first point is the same as the last to create a closed wire. // The wire body will contain 4 edges and 4 vertices. SPAposition * pos_array = ACIS_NEW SPAposition[5]; pos_array[0] = SPAposition(-5.0, -5.0, z_coord - 1.0); pos_array[1] = SPAposition(-5.0, 5.0, z_coord + 1.0); pos_array[2] = SPAposition( 5.0, 5.0, z_coord - 1.0); pos_array[3] = SPAposition( 5.0, -5.0, z_coord + 1.0); pos_array[4] = SPAposition(-5.0, -5.0, z_coord - 1.0); result = api_make_wire (my_body, 5, pos_array, my_body); CHECK_RESULT ACIS_DELETE [] pos_array; // De-allocate the array. // Convert the wire body into a solid body. // This is accomplished by covering the wire with a face. // The solid body will contain one single-sided face. WIRE * my_wire = my_body->lump()->shell()->wire(); result = api_cover_wire (my_wire, *(surface*)NULL_REF, my_face); CHECK_RESULT API_END if (!result.ok()) return result; // Convert the solid body into a sheet body. // The sheet body will contain one double-sided face. result = api_body_to_2d (my_body); CHECK_RESULT2 // Split the face into two faces along an isoparametric curve. // We will perform the split in the u-direction at the mid // parameter of the surface. result = api_split_face (my_face, TRUE, TRUE, 0.5); CHECK_RESULT2 return result; } void my_debug_history_stream() { // This function debugs the default History Stream. HISTORY_STREAM * hs = NULL; outcome result = api_get_default_history(hs); if (!result.ok()) return; printf ("\n******************************************************\n"); DELTA_STATE * root_ds = hs->get_root_ds(); printf ("The root Delta State is 0x%x\n", (long)root_ds); DELTA_STATE * current_ds = hs->get_current_ds(); printf ("The current Delta State is 0x%x\n", (long)current_ds); DELTA_STATE * active_ds = hs->get_active_ds(); printf ("The active Delta State is 0x%x\n\n", (long)active_ds); // Set the amount of debug information desired const int debug_level = 1; // Examine the Active Delta State and // follow the next pointers from it. // (The next pointers should point backward.) if (active_ds) { printf ("Details of the active Delta State\n\n"); my_debug_delta_state(active_ds, debug_level); // Follow the next Delta State pointers DELTA_STATE * this_ds = active_ds->next(); if (this_ds) printf ("Scanning the next Delta State pointers\n\n"); while (this_ds) { my_debug_delta_state(this_ds, debug_level); this_ds = this_ds->next(); } // Examine the Active Delta State's partner and // follow the next pointers from it. // (The next pointers should point forward.) if (active_ds != active_ds->partner()) { DELTA_STATE * partner_ds = active_ds->partner(); printf ("Details of the active Delta State's partner\n\n"); my_debug_delta_state(partner_ds, debug_level); // Follow the next Delta State pointers this_ds = partner_ds->next(); if (this_ds) printf ("Scanning the next Delta State pointers\n\n"); while (this_ds) { my_debug_delta_state(this_ds, debug_level); this_ds = this_ds->next(); } } } } void my_debug_delta_state( DELTA_STATE * ds, // in: The Delta State to debug int debug_level // in: Amount of debug information // 0 : No Bulletin info // 1 : Summary Bulletin info // 2 : Detailed Bulletin info ) { // This function prints information about a given Delta State. // If brief is TRUE, it prints information only about the Delta State. // If brief is FALSE, it prints additional information about the // Bulletin Boards and Bulletins within the Delta State. printf("Delta State 0x%x\n", (long)ds); printf("Previous Delta State 0x%x\n", (long)ds->prev()); printf("Next Delta State 0x%x\n", (long)ds->next()); printf("Partner Delta State 0x%x\n", (long)ds->partner()); printf("Rolls %s ", ds->backward() ? "backward" : "forward"); printf("to state %d ", (int)ds->to()); printf("from state %d\n", (int)ds->from()); BULLETIN_BOARD * bb = ds->bb(); if (bb == NULL) printf("\tContains No Bulletin Boards\n"); while (bb) { printf("\tBulletin Board 0x%x\n", (long)bb); if (debug_level) { BULLETIN * b = bb->start_bulletin(); // Counters for summary debugging. // We could have many more, // but this should demonstrate the concept. int created_faces = 0; int changed_faces = 0; int deleted_faces = 0; int created_edges = 0; int changed_edges = 0; int deleted_edges = 0; int created_coedges = 0; int changed_coedges = 0; int deleted_coedges = 0; while (b) { ENTITY * old_ent = b->old_entity_ptr(); ENTITY * new_ent = b->new_entity_ptr(); BULLETIN_TYPE b_type = b->type(); if (debug_level == 1) { switch (b_type) { case CREATE_BULLETIN: if (is_FACE(new_ent)) created_faces++; else if (is_EDGE(new_ent)) created_edges++; else if (is_COEDGE(new_ent)) created_coedges++; break; case CHANGE_BULLETIN: if (is_FACE(new_ent)) changed_faces++; else if (is_EDGE(new_ent)) changed_edges++; else if (is_COEDGE(new_ent)) changed_coedges++; break; case DELETE_BULLETIN: if (is_FACE(old_ent)) deleted_faces++; else if (is_EDGE(old_ent)) deleted_edges++; else if (is_COEDGE(old_ent)) deleted_coedges++; break; } } else if (debug_level == 2) { const char * ent_type_str = (old_ent) ? old_ent->type_name() : (new_ent) ? new_ent->type_name() : "unknown"; const char * b_type_str = (b_type == CREATE_BULLETIN) ? "CREATE" : (b_type == DELETE_BULLETIN) ? "DELETE" : "CHANGE"; printf("\t\tBulletin 0x%x\n", (long)b); printf("\t\t%s %s, old_ent = 0x%x, new_ent = 0x%x\n", b_type_str, ent_type_str, (long)old_ent, (long)new_ent); } b = b->next(); } if (debug_level == 1) { printf("\t\tCreated %d faces %d edges and %d coedges\n", created_faces, created_edges, created_coedges); printf("\t\tChanged %d faces %d edges and %d coedges\n", changed_faces, changed_edges, changed_coedges); printf("\t\tDeleted %d faces %d edges and %d coedges\n", deleted_faces, deleted_edges, deleted_coedges); } } bb = bb->next(); } printf("\n"); } void my_find_active_bodies( ENTITY_LIST & bodies // out: list of active bodies ) { // Scan the History Stream from the active state to the root state // and find all bodies that have been created but not deleted. // Create lists of bodies that have been created and deleted ENTITY_LIST all_created_bodies; ENTITY_LIST all_deleted_bodies; HISTORY_STREAM * hs = NULL; outcome result = api_get_default_history(hs); if (!result.ok()) return; // Because all Delta States from the active state to the root state // roll backward, we follow "next" Delta State pointers. DELTA_STATE * ds = hs->get_active_ds(); while (ds) { BULLETIN_BOARD * bb = ds->bb(); while (bb) { BULLETIN * b = bb->start_bulletin(); while (b) { ENTITY * old_ent = b->old_entity_ptr(); ENTITY * new_ent = b->new_entity_ptr(); BULLETIN_TYPE b_type = b->type(); if (b_type == CREATE_BULLETIN && is_BODY(new_ent)) all_created_bodies.add(new_ent); else if (b_type == DELETE_BULLETIN && is_BODY(old_ent)) all_deleted_bodies.add(old_ent); b = b->next(); } bb = bb->next(); } ds = ds->next(); } // Add all created but not deleted bodies to the list to be returned. bodies.clear(); for (int i = 0; i < all_created_bodies.count(); i++) { ENTITY * ent = all_created_bodies[i]; if (all_deleted_bodies.lookup(ent) == -1) bodies.add(ent); } } int my_initialization() { // Start ACIS. outcome result = api_start_modeller(0); CHECK_RESULT3 // Call the licensing function to unlock ACIS. unlock_spatial_products_<NNN>(); // Initialize all necessary components. result = api_initialize_kernel(); CHECK_RESULT3 return 0; } int my_termination() { // Terminate all necessary components. outcome result = api_terminate_kernel(); CHECK_RESULT3 // Stop ACIS and release any allocated memory. result = api_stop_modeller(); CHECK_RESULT3 return 0; } Discussion The behavior of this program can be modified by modifying the logical variables at the top of do_something. If the initial model is generated, and no rolling is performed, and no additional bodies are generated, and our debug function is used, the output should be identical to the original program. The diagram below depicts the history stream after generating the three bodies. Notice that each delta state's "next" and "previous" pointers correspond to the direction that the delta state rolls, not to "future" and "past" delta states. Original History Stream Modify the program to generate the initial model and roll back two states. (Actually, this is the state of the example program above.) The debug information obtained by executing the program should look similar to the original debug information; however, there are some significant differences. The active delta state is now the one representing the construction of the first body. The two delta states after the active state are now in a rolled back state. If they are rolled again they will roll forward. The contents of the bulletin boards and the order of the bulletin boards on these delta states have been reversed. When we rolled back the bulletin boards we deleted what we constructed on them and undid the changes we made on them. (If you are curious, you can change the debug_level to 2 and see how the bulletins have been changed.) The diagram below depicts the history stream after generating the three bodies and then rolling back two states. Modified History Stream If after rolling back two states we were to construct two additional bodies, the model would be very similar to our initial model, except the history stream would be slightly different. The diagram below depicts the history stream after generating the three bodies, then rolling back two states, and then generating two more bodies. Compare this diagram to the diagram of the original model. The two rolled back delta states have been deleted and two new delta states have been added. Modified History Stream Up to this point we have dealt exclusively with linear history streams. As we have said it is possible to use a branched history stream, which contains all of the changes made in a model (unless pieces of the history stream are pruned away.) The diagram below depicts the history stream after generating the three bodies, then rolling back two states, and then generating two more bodies, if branched history streams are allowed. As you can see all of the delta states are contained in this history stream. To generate the debug output for this case you should use the ACIS function for debugging the history stream rather than my_debug_history_stream because my_debug_history_stream cannot traverse a branched history stream. Modified History Stream By modifying the logical variables in do_something, modifying the debug_level in my_debug_history_stream, and modifying do_something to perform additional modifications to the model you can explore a wide range of possible history streams. Take some time and play with the program to simulate modeling sessions of interest. Are there any differences in the history stream between (a) generating three bodies, and (b) generating three bodies, rolling backward two states, and rolling forward two states? What happens if you generate three bodies, roll backward two states, and attempt to roll back two more states? (Can you roll back over the root state?) Our example program does not demonstrate many history stream related functions. Feel free to modify the program to explore the behavior of the following functions we have discussed: We would recommend not using set_logging or api_delete_ds. set_logging can make it impossible to recover from exceptions. api_delete_ds can cause memory leaks if used improperly. Related Topics ACIS supports a tagging system for entities, which is a more persistent means to access entities than is possible with physical addresses. Refer to Tags and Tag Managers. Tutorial 11: Attributes In previous tutorials we have discussed the behavior and data structures of ACIS. We have reached the point where we are ready to describe how you can tailor ACIS to meet the needs of your application; in particular, how you can add application-specific data to ACIS and connect ACIS with your application. ACIS is object-oriented so theoretically you could derive your own classes from the existing geometry and topology classes, but in practice this is difficult to do. The preferred means is to attach information to the existing classes using attributes. This tutorial describes how you can use attributes to add application-specific data to the ACIS model and to connect the ACIS data structure with your application's data structure. An Introduction to Attributes An attribute is a specialized type of entity that is used to attach data to entities. An entity may have zero or more attributes. Attributes can carry simple data, pointers to other entities, or links to application specific variable length data. ACIS uses many system attributes, but applications may also define their own attributes. Some Terminology We refer to attributes that contain simple data types (for instance, integers, doubles, or strings) as being simple attributes. Attributes that contain pointers to ENTITYs are referred to as complex attributes. Attributes that contain or point to more complex data (for instance, meshes, texture maps, stress calculation results, or topology classes in your applications) are referred to as bridge attributes. Instruction attributes are use to control ACIS algorithms (for instance one of the blending algorithms uses attributes to specify which edges to blend, the sizes of blends, etc.) Attributes can also be categorized as being user-defined attributes or system-defined attributes. A user may define simple, complex, or bridge attributes, but instructional attributes are system-defined. Another term you may hear is generic attribute. Generic attributes are predefined attributes that you may use in your application. An organization attribute is an organization-specific attribute that is used as a base class for all of the attributes classes derived by an organization. Class Derivations Attributes are implemented in the ATTRIB class. The ATTRIB class is derived from the ENTITY class; therefore, ATTRIBs inherit all of the functionality of ENTITYs, including copy, save and restore, and rollback. All system-defined and user-defined attributes are derived from the ATTRIB class. In addition to the data and functions in the ENTITY class, the ATTRIB class provides a pointer to the ENTITY to which the ATTRIB is attached. (The ENTITY to which an ATTRIB is attached is often called the owner of the ATTRIB.) We did not mention it previously, but each ENTITY also contains a pointer to an ATTRIB. Each ATTRIB also contains a next and previous pointer, so each ENTITY actually contains a pointer to a linked list of ATTRIBs. The easiest way to find a specific type of ATTRIB on an ENTITY is to use find_attrib(ENTITY*,...) which searches for the first ATTRIB on an ENTITY of a specified type. To search for subsequent ATTRIBs of the same type on an ENTITY you can use find_next_attrib(ATTRIB*,...). Both of these functions require being able to uniquely specify the type of an attribute, which we will discuss shortly. ATTRIBs also contain a number of member functions and data that relate to how the attribute should behave when various operations are performed on the owning entity (for instance, what should happen to a specific type of ATTRIB when its owner is copied.) The next level of derivation below the ATTRIB level is the component or organization level, meaning the next level of derivation relates to the ACIS component that uses the attribute, or in the case of a user-defined attribute, to the organization that defined the attribute. Examples of the next level of system-defined attributes include ATTRIB_SYS, ATTRIB_CT, and ATTRIB_ST. User-defined, organization attributes require a name unique to your organization. The name will be "ATTRIB_" followed by a two or three letter sentinel. You should contact Spatial’s Customer Support to request a unique sentinel for your organization if you desire to derive attributes. (Having unique sentinels provides a means of uniquely identifying attribute classes when restoring SAT and SAB files.) All of your instantiable, user-defined attributes will be derived from your organization attribute class. The diagram below depicts the derivation levels of attributes. The classes depicted by shaded boxes are defined within ACIS: those depicted by unshaded boxes must be defined by the application. Attribute Derivation Generic Attributes As an example of attribute derivation and to give you an overview of generic attributes, let's take a brief look at the derivation levels of generic attributes. (Generic attribute classes are defined within ACIS.) ATTRIB_GENERIC, the base class for all generic attributes, is derived from ATTRIB. ATTRIB_GENERIC does not provide any additional member data or functions that are not present in the ATTRIB class. ATTRIB_GEN_NAME is derived from ATTRIB_GENERIC. ATTRIB_GEN_NAME provides a name string, and data and functions that specify what action should be taken when the owner of the attribute is split, merged, translated, or copied. Eight classes are derived from ATTRIB_GEN_NAME. They are: 1. ATTRIB_GEN_POINTER contains an ENTITY pointer. 2. ATTRIB_GEN_ENTITY contains an ENTITY pointer. The difference between an ATTRIB_GEN_ENTITY and an ATTRIB_GEN_POINTER is that it is assumed the ATTRIB_GEN_ENTITY "owns" the ENTITY it points to, whereas ATTRIB_GEN_POINTER just points to an ENTITY. This affects what happens to the ENTITY when the ATTRIB is copied, lost, or transformed. 3. ATTRIB_GEN_INTEGER contains an integer. 4. ATTRIB_GEN_REAL contains a double. 5. ATTRIB_GEN_POSITION contains a SPAposition. 6. ATTRIB_GEN_VECTOR contains a SPAvector. 7. ATTRIB_GEN_STRING contains a character string. 8. ATTRIB_GEN_WSTRING contains a wide character string. As an example of how you might use a generic attribute: If your application frequently used the center of mass of the BODYs in your model, you could create an instance of a ATTRIB_GEN_POSITION, store the center of mass of a BODY in it, store the string "center of mass" in it, and attach it to a BODY. Your application could then use the center of mass stored in this attribute rather than recalculating it each time it was needed. Of course this attribute would not be updated whenever the shape of the BODY was changed, so your application would need to implement functions that update or invalidate the center of mass value stored in the attribute whenever the shape of the BODY changed. (Invalidation of cached values is commonly used in ACIS. Instead of recalculating a value every time an entity changes, the value is simply marked as being invalid. The value is not recalculated again until the value is requested by some function. This saves needless recalculations and makes algorithms more efficient.) Changes to the Attribute's Owner If the owner of an attribute is modified, the data in the attribute may need to be modified. In fact, the attribute itself may need to be copied, or lost, or moved to another entity. To facilitate the modification of the attribute and its data when its owner is modified, the ATTRIB class defines a number of virtual functions. These functions contain application-specific code that controls the behavior of the attribute when the owner of the attribute is modified. Member Function Description copy_owner Specifies the action to be taken when the attribute's owner is being copied. The default action is to do nothing. from_tolerant_owner Specifies the action to be taken when the attribute's owner is being replaced by a non-tolerant entity and will be deleted. The default action is to move the attribute onto the given non-tolerant ENTITY if the attribute is movable. lop_change_owner Specifies the action to be taken when the attribute's owner is being changed during a local operation. The default action is to do nothing. merge_owner Specifies the action to be taken when the attribute's owner is being merged with another entity. The default action is to do nothing. replace_owner Specifies the action to be taken when the attribute's owner is being replaced by another entity. The default action is to do nothing. replace_owner_geometry Specifies the action to be taken when the geometry of the attribute's owner is being replaced. The default action is to do nothing. reverse_owner Specifies the action to be taken when the sense bit of the attribute's owner is being reversed. The default action is to do nothing. split_owner Specifies the action to be taken when the attribute's owner is being split into two entities. The default action is to do nothing. to_tolerant_owner Specifies the action to be taken when the attribute's owner is being replaced by a tolerant entity and will be deleted. The default action is to move the attribute onto the given tolerant ENTITY if the attribute is movable. trans_owner Specifies the action to be taken when the attribute's owner is being transformed. The default action is to do nothing. warp_owner Specifies the action to be taken when the attribute's owner is being warped. The default action is to do nothing. If you choose not to implement any of the above member functions, you can specify the behavior using a set of predefined behaviors, or you can accept the default behavior. A number of member functions exist for specifying predefined behaviors. These are listed in the table below. Each of these functions takes an enumeration as an argument. For example, set_copy_owner_action takes a copy_action as an argument. The copy_action specifies whether the attribute should remain on the entity, or be copied to the new entity, or be lost. A complete list of these enumerations is available in the Attributes article. Note: The functions to specify a predefined behavior operate on instances of attributes, whereas the behavior specified in the <action>_owner functions apply to the entire class. Member Function Description set_copy_owner_action(const copy_action) Specifies the action to be taken when the attribute's owner is being copied. set_from_tolerant_owner_action(const tolerant_action) Specifies the action to be taken when the attribute's owner is being replaced by a non-tolerant entity and will be deleted. set_lop_change_owner_action(const geometry_changed_action) Specifies the action to be taken when the attribute's owner is being changed during a local operation. set_merge_owner_action(const merge_action) Specifies the action to be taken when the attribute's owner is being merged with another entity. set_rep_owner_geom_action(const geometry_changed_action) Specifies the action to be taken when the geometry of the attribute's owner is being replaced. set_replace_owner_action(const replace_action) Specifies the action to be taken when the attribute's owner is being replaced by another entity. set_reverse_owner_action(const geometry_changed_action) Specifies the action to be taken when the sense bit of the attribute's owner is being reversed. set_split_owner_action(const split_action) Specifies the action to be taken when the attribute's owner is being split into two entities. set_to_tolerant_owner_action(const tolerant_action) Specifies the action to be taken when the attribute's owner is being replaced by a tolerant entity and will be deleted. set_trans_owner_action(const trans_action) Specifies the action to be taken when the attribute's owner is being transformed. set_warp_owner_action(const geometry_changed_action) Specifies the action to be taken when the attribute's owner is being warped. If you set the behavior and define the member function, ACIS will use the behavior implemented in your member function definition rather than the set behavior. Nevertheless, it may be valuable to set the behavior and define the member function. If your attribute class is saved to a SAT or SAB file, its set behavior is saved with it. If the SAT file is restored by another ACIS-based application that does not contain the definition of your attribute class, the ACIS-based application will use the set behavior. Using this mechanism you could, for example, specify that instances of an attribute class should always be lost whenever their owner changes. This could prevent the attributes from containing invalid data. How are the above functions called? Any code within ACIS that modifies an entity must call a function to notify the entity's attributes that a change is occurring. The type of modification dictates which notification function is called. (For example, if an EDGE is being converted into a TEDGE, the EDGE's attributes are notified by calling to_tolerant_attrib(EDGE*, TEDGE*).) If your application modifies the ACIS data structure using direct interface calls, your application may need to call these functions too. If not, this discussion should help you to distinguish between the global, notification functions (listed below) and the application-specific, behavior-specifying, virtual functions (described above.) Global Function Description copy_attrib Notifies all of the attributes of an entity that their owner is being copied. from_tolerant_attrib Notifies all of the attributes of an entity that their owner is being replaced by a non-tolerant entity and will be deleted. lop_change_attrib Notifies all of the attributes of an entity that their owner is being changed during a local operation. merge_attrib Notifies all of the attributes of an entity that their owner is being merged with another entity. replace_attrib Notifies all of the attributes of an entity that their owner is being replaced by another entity. replace_geometry_attrib Notifies all of the attributes of an entity that the geometry of their owner is being replaced. reverse_attrib Notifies all of the attributes of an entity that the sense bit of their owner is being reversed. split_attrib Notifies all of the attributes of an entity that their owner is being split into two entities. to_tolerant_attrib Notifies all of the attributes of an entity that their owner is being replaced by a tolerant entity and will be deleted. trans_attrib Notifies all of the attributes of an entity that their owner is being transformed. warp_attrib Notifies all of the attributes of an entity that their owner is being warped. Specifying Additional Attribute Functionality In addition to the behavior specification functions described above, there are other aspects of an attribute class's functionality that you can specify. In particular, you can specify if an attribute is copyable, deletable, duplicatable, moveable, or savable. This is accomplished by overloading the virtual functions: copyable, deletable, duplicatable, moveable and savable. The functionality of these functions is described in the following table. Member Function Description copyable Indicates whether this attribute should be copied when the owning entity is copied. The default is to call duplicatable. deletable Indicates whether this attribute is independently deletable of its owner. The default and suggested behavior is to return FALSE. duplicatable Indicates whether this attribute can be duplicated. The default is TRUE. moveable Indicates whether this attribute can be moved from one owner to another. The default is to call copyable. savable Indicates whether this attribute should be saved when the owning entity is saved. The default is to call duplicatable. What is the difference between copyable and duplicatable? It is too bad that these function names are so similar. duplicatable means that the attribute can be copied or saved. By defining the duplicatable member function you can turn on or off the ability to be copied or saved. If you want an attribute to be copied but not saved, or vice versa, you can define copyable and/or savable. Types of Attributes Attributes inherit a typing mechanism from the ENTITY class. The type of an ENTITY is a symbol the signifies a specific class of ENTITY. For instance, EDGE_TYPE is the type associated with the EDGE class. Notice that we are referring to the symbol EDGE_TYPE and not the value of this symbol. The values of the symbols associated with classes derived from ENTITY are assigned at run-time and depend upon the object modules that are linked into an application. In addition to a type each class derived from ENTITY also has a level, which specifies it derivation level from the ENTITY class. For instance, EDGE_LEVEL is the level of the EDGE class. The level of a class does not change from application to application, but it is recommended that you use the level symbol for clarity in programming. The type and level of an ENTITY are used by ENTITY::identity(const int level). identity returns the type of an ENTITY at a specific level. For example, if you had a pointer to a TEDGE, ent, and you queried ent->identity(TEDGE_LEVEL) it would return TEDGE_TYPE. If you had a pointer to a TEDGE, ent, and you queried ent->identity(EDGE_LEVEL) it would return EDGE_TYPE. If you had a pointer to a non-tolerant EDGE, ent, and you queried ent->identity(TEDGE_LEVEL) it would return unknown. A value of unknown means the ENTITY is not defined at that level, which in this case means the EDGE is not a TEDGE. The is_<entity_class>(const ENTITY *) functions we have mentioned and used in previous examples are implemented using the identity member functions. For example, is_EDGE(const ENTITY *) simply determines if the type of an ENTITY at the EDGE_LEVEL is EDGE_TYPE. It will return TRUE for EDGEs and TEDGEs, and it will return FALSE for all other ENTITYs. For more information on determining the type of an ENTITY, refer to Identifying Model Objects. The type and level of an attribute are used by the following functions that search the attribute list of an ENTITY to find an attribute of the specific type: ATTRIB *find_attrib( const ENTITY *owner, int subtype = -1, int subsubtype = -1, int subsubsubtype = -1, int subsubsubsubtype = -1 ); ATTRIB *find_next_attrib( ATTRIB const *att, int subtype = -1, int subsubtype = -1, int subsubsubtype = -1, int subsubsubsubtype = -1 ); Example 1: Assume you created an organization attribute class for your organization called ATTRIB_ABC and you specified the type and level to be ATTRIB_ABC_TYPE and ATTRIB_ABC_LEVEL. Also assume you created a specific attribute class called ATTRIB_COLOR and you specified the type and level to be ATTRIB_COLOR_TYPE and ATTRIB_COLOR_LEVEL. If you wanted to search the attribute list of an ENTITY, ent, to find the first instance of an ATTRIB_COLOR you would call: ATTRIB * my_att = find_attrib(ent, ATTRIB_ABC_TYPE, ATTRIB_COLOR_TYPE); To find the next such attribute on the ENTITY you would call: ATTRIB * next_att = find_next_attrib(my_att, ATTRIB_ABC_TYPE, ATTRIB_COLOR_TYPE); These functions will return NULL if they fail to find an attribute meeting the given specification. Example 2: If you wanted to find any attribute that was defined by your organization you could type: ATTRIB * my_att = find_attrib(ent, ATTRIB_ABC_TYPE); and ATTRIB * next_att = find_next_attrib(my_att, ATTRIB_ABC_TYPE); There are two other functions that allow you to search for an instance of a "leaf" attribute class; that is, instance of attribute class from which no other attribute classes have been derived. These functions do not require the specification of the attribute type at each level of derivation and instead are based on the fact that the types of attributes have unique values. ATTRIB *find_leaf_attrib( const ENTITY * owner, int attrib_type ); ATTRIB *find_next_leaf_attrib( ATTRIB const * attrib ); Attaching and Removing In addition to searching for and querying attributes, applications that use attributes need to attach attributes to entities and remove them from entities. An attribute is typically attached to an entity when the the attribute is constructed; therefore, the constructor for each attribute class contains an argument that specifies the owner of the attribute. As mentioned previously each entity possesses a linked list of attributes. To remove an attribute from an entity's list of attributes, you call the attribute's unhook method. If the attribute is of no further use, you can call the attribute's lose method to delete it. As we mentioned in the Memory Management Tutorial, you should never directly delete an instance of any class derived from ENTITY, because entities are maintained by the history mechanism. The use of unhook and lose is demonstrated below. ATTRIB_COLOR_TYPE *my_att = (ATTRIB_COLOR_TYPE *)find_attrib( owner_ent, ATTRIB_ABC_TYPE, ATTRIB_COLOR_TYPE ); if ( my_att != NULL ) { my_att->unhook(); my_att->lose(); } Save and Restore Instances of attribute classes will be saved to SAT or SAB files when their owner is saved, if the attribute's savable method returns TRUE. When an attribute is saved its derivation from ENTITY is saved, along with all the saved data members for every level of derivation. This is true for all classes derived from ENTITY, not just attributes. Example: Assume we were to derive a class ATTRIB_ABC from ATTRIB, and derive ATTRIB_COLOR from ATTRIB_ABC. Also assume we were to associate the name "abc" with ATTRIB_ABC and "color" with ATTRIB_COLOR. In addition, assume that ATTRIB_COLOR contains a single data member, an integer, which is also to be saved and restored. If an instance of the ATTRIB_COLOR is saved, the derivation from entity is saved as "color-abc-attrib." This indicates that "color" is derived from "abc", "abc" is derived from "attrib," and "attrib" is derived from ENTITY. Following this derivation information is the saved data for ENTITY, followed by the saved data for ATTRIB, followed by the saved data for ATTRIB_ABC, followed by the saved data for ATTRIB_COLOR. A record in the SAT file might look something like the following. -1 color-abc-attrib-1 -1 $-1$-1 $0 2 1 2 1 1 1 1 1 1 1 1 1 0 1 0 1 1 1 1 # To read the above record you must realize that each entity has its own record, which is uniquely numbered, and that pointers to other entities have been converted into a reference to its record number, preceded by a '$' symbol. Because 0 (zero) is a valid record number, $-1 is the symbol for a NULL pointer. The first field in the above record is the sequence number, negated. (Adding sequence numbers to SAT files is optional, but it makes human interpretation of a SAT file much easier.) The second field is the string denoting the derivation from ENTITY. Following this is the data for ENTITY. In this case it consists of "$-1 -1" which is the attribute's attribute pointer and tag. Following this is the data for ATTRIB. In this case it consists of "$-1$-1 $0 2 1 2 1 1 1 1 1 1 1 1 1 0 1 0 1 1 1" which is the attribute's next pointer, previous pointer, and owner pointer, followed by the 18 fields for set behaviors. Following this is the data for ATTRIB_ABC. Organization attribute classes do not have any data members so this is non-existent. Following this is the data for ATTRIB_COLOR, which consists of "1" which is the color value. Following this is the end of record symbol '#'. Specifying what should be saved and restored for an attribute class has been simplified through the use of macros. In fact, much of the declaration and definition of attribute classes has been simplified through the use of macros. The macros associated with save and restore are SAVE_DEF and RESTORE_DEF. These are used in conjunction with a number of functions for writing and reading fields in records. Some of the more commonly used functions are enumerated below. These functions and many more are declared in fileio.hxx (with the exceptions of write_ptr and read_ptr which are declared in savres_small.hxx.) Writing Function Reading Function Description write_enum read_enum Writes or reads an enumeration. write_int read_int Writes or reads an integer value. For platforms on which integers and longs are different sizes, refer to the Library Reference description of these functions. write_interval read_interval Writes or reads an interval. write_logical read_logical Writes or reads a logical. write_long read_long Writes or reads a long. write_position read_position Writes or reads a position. write_ptr read_ptr Writes or reads an ENTITY pointer. write_real read_real Writes or reads a double. write_string read_string Writes or reads a string. write_vector read_vector Writes or reads a vector. write_wstring read_wstring Writes or reads a wide character string. Note: You should avoid using the characters '$', '#', '{', and '}' in strings because they may interfere with the SAT parser in applications that do not contain the definitions of your attributes.

The mechanisms to save and restore entity pointers deserve a little more discussion than those of other data types because they are slightly more complicated. The save and restore mechanisms for entities do not work on isolated entities but rather on sets of connected entities. In other words, if you were to save a body you would get all of the other entities connected to the body. The save process consists of: list traversal, list building, and file writing. The save process begins with a list of entities. As each of these entities is written to the save file any entities pointed to by the entity are appended to the list. Processing continues as long as there are entities in the list. The restore process can be decomposed into two phases: file reading and fixing pointers. As the file is read and the entities are constructed an array of entities is constructed. This array maintains a correspondence between the record number in the file and the address of the new entities. After all of the entities have been read and constructed their entity pointers must be adjusted, because when they were initially constructed their entity pointers were set to record numbers. For example, assume you read in a coedge that pointed to an edge that was entity number 47 in the file. When the coedge was constructed its edge pointer would contain a 47. After all the entities were read and constructed the coedge's edge pointer must be changed so that it points to the correct edge address. Thus, the 47 must be changed to the address of the 47th entity.

On a side note, the pointer fixing process also occurs when copying. The copy process consists of three phases: scanning, copying, and fixing pointers. First it builds a list of all the entities to be copied, copies them, and finally fixes their pointers. All of these actions (relating to saving, restoring, and copying) are specified with the aid of macros.

For additional information on the save and restore mechanism, refer to SAT Save and Restore. For additional information on the copy mechanism, refer to Copying Objects. For additional information on all the macros associated with ENTITY derivation, refer to Entity Derivation Macros.

Debugging

In addition to save and restore functionality, attributes inherit the ability to be debugged from the ENTITY class. A developer typically debugs an object in the model by debugging the top level topological entity in the object (often a BODY) and requesting a top-down dump of all connected entities. This is usually accomplished by calling debug_entity on the top level topological entity. For more information on the use of debug_entity and the output format of common entity types, refer to Debug File.

When you design a specific attribute class you choose what debug information is output for that class. Typically, you will simply print out the data members of the class, but in the case of a bridge attribute, you may choose to print out more or less than that. There are several functions to assist you in printing the debug information. Using these functions maintains a consistent format to the debug output. The most commonly used functions for debugging attributes are listed in the table below. Additional information on these and other debugging related functions is available in the Debugging article.

Function Description
debug_int Prints an integer with the given title to the output file.
debug_new_pointer Prints an entity pointer with the given title to the output file. This function also appends the entity pointer to the list of entities being debugged, if it not already in the list.
debug_old_pointer Prints an entity pointer with the given title to the output file. This function does not append the entity pointer to the list of entities being debugged.
debug_real Prints a double with the given title to the output file.
debug_string Prints a string with the given title to the output file.

To distinguish between debug_new_pointer and debug_old_pointer: if the ENTITY pointed to most likely has not been debugged yet, you should call debug_new_pointer; if you are certain the ENTITY pointed to has already been debugged, you should call debug_old_pointer. You may also want to call debug_old_pointer if you do not want the ENTITY that is pointed to be debugged (for instance, if an attribute on one BODY points to another BODY, you may not want the entire other body to be debugged.) There is another related function, debug_sib_pointer, which is seldom used when debugging specific attribute data, but is used to debug a sibling of an ENTITY (for instance, the next face in a list of faces or the next coedge in a loop of coedges.)

Note: When specifying the data to be debugged in a specific attribute class, you do not need to specify that the next attribute, previous attribute, or owning entity needs to be debugged. The debug information for those entities is specified by the ATTRIB class. You only need to specify the debug information for the data members you add to the class.

Attributes and Patterns

Attributes are typically owned by entities. If an entity that owns an attribute becomes a seed entity of a pattern, there may be an issue regarding propagating the attribute to elements of the pattern. For instance, a simple attribute could contain a data member specifying the color or surface finish of a face. Such an attribute could be propagated to each of the clones of the face in the pattern. Alternatively, a simple attribute could contain a data member that records the area of the face. If the transformations in the pattern affect the size of the face, the attribute containing the face area value could not simply be copied to each of the clones of the face in the pattern. Some action much be taken to invalidate and recalculate the face area value for the new faces. A developer might choose not to propagate such an attribute. To control whether or not an attribute is propagated during the expansion of a pattern, you can overload the pattern_copyable member function. The default behavior of pattern_copyable is to call copyable.

If a pattern is expanded and attributes are placed on non-seed entities (that is, entities that are created during the process of expanding the pattern), there may be an issue with saving and restoring the attributes if the pattern is collapsed. This typically occurs if the attribute on a non-seed entity contains a pointer to an entity in the expanded pattern other than its owner. Such attributes would be categorized as being pattern-incompatible. If an attribute is pattern-incompatible, its pattern cannot be collapsed; therefore, if a model contains a pattern with pattern-incompatible attributes and the model is saved, the pattern will be removed prior to the actual save operation. (In other words, the pattern of elements will be left in its expanded state and the pattern object will be removed from the model.) ACIS cannot determine whether or not an attribute is pattern-compatible. The application developer must specify whether an attribute class is pattern-compatible or not. This is accomplished by overloading the definition of the pattern_compatible member function. The pattern_compatible member function affects how patterns are saved in SAT files. This applies only to attributes that are savable. The default behavior of pattern_compatible is to return FALSE.

In the following subsections we describe and provide examples demonstrating how to create organization attribute classes and specific attribute classes. In all of the examples we present the ACIS style for the creation of the attribute class. We strongly recommend you use this style for the creation of your attributes.

Organization Attribute Classes

The creation of an organization attribute class is highly simplified by the use of two macros for the declaration and definition of the class: MASTER_ATTRIB_DECL and MASTER_ATTRIB_DEFN. MASTER_ATTRIB_DECL declares all the member data and functions for an organization attribute class and MASTER_ATTRIB_DEFN provides the definitions. Very little is needed beyond these macros.

The ACIS style for the creation of an attribute class (or any class derived from ENTITY) is to place the declaration of the class in its own header file and the definition of its member functions in its own source file. To prevent header files from being included multiple times in source files, each header file contains a #if block, which checks to see if a symbol has been previously defined. Roughly speaking, if the symbol has been previously defined, the attribute declaration will not be inserted into the file; if the symbol has not been previously defined, the attribute declaration will be inserted into the file. This implies that the symbol should be unique. The ACIS style is to use the name of the class as the symbol. Inside the #if block are three sections: in the first are the necessary header files to be included, in the second the type and level are declared and #defined, in the third is the MASTER_ATTRIB_DECL. The two arguments to MASTER_ATTRIB_DECL are: (1) the class being declared and (2) the ACIS library in which the class should be contained. An example of a organization attribute header file is shown below. To modify this for an actual application you would need to replace all occurrences of "ABC" with your organization's sentinel. (For the purposes of a demonstration program using "ABC" is fine.)

// This is the header file for the organization attribute class ATTRIB_ABC.

#if !defined(ATTRIB_ABC_CLASS)
#define ATTRIB_ABC_CLASS

#include "dcl_kern.h"
#include "attrib.hxx"

extern int ATTRIB_ABC_TYPE;
#define ATTRIB_ABC_LEVEL (ATTRIB_LEVEL + 1)

MASTER_ATTRIB_DECL( ATTRIB_ABC, NONE )

#endif

An example of a source file for an organization attribute class is presented below. The source file contains the necessary header files, the definition of four macros defining the class, its parent class, and the ACIS libraries they are contained within. (These four macros are used whenever any entity class is defined.) Following these four macros is the definition of the name string used in a save file and the MASTER_ATTRIB_DEFN macro. The single argument to MASTER_ATTRIB_DEFN is the string used to identify the class in a debug file. If you were to modify this file for your organization, you would replace all occurrences of "ABC" with your organization's sentinel in upper case, and all occurrences of "abc" with your organization's sentinel in lower case. (Again, for the purposes of a demonstration program using "ABC" is fine.)

Organization Attribute Source File: at_abc.cpp
#include <stdio.h>
#include "acis.hxx"
#include "dcl_kern.h"
#include "datamsc.hxx"
#include "at_abc.hxx"

// Define macros for this attribute and its parent, to provide
// the information to the definition macro.
#define THIS() ATTRIB_ABC
#define THIS_LIB NONE
#define PARENT() ATTRIB
#define PARENT_LIB KERN

// Identifier used externally to identify a particular entity
// type. This is only used within the save/restore system for
// translating to/from external file format, but must be unique
// amongst attributes derived directly from ATTRIB, across all
// application developers.
#define ATTRIB_ABC_NAME "abc"

MASTER_ATTRIB_DEFN( "abc master attribute" )

That is all that is required to declare and define an organization attribute class.

Specific Attribute Classes

In the following subsections we describe how to create simple, complex, and bridge attributes classes. Because specific attribute classes contain application-specific data and functions their creation cannot be as simplified as organization attribute classes; however, the creation of specific attribute classes is greatly simplified through the use of macros.

The declaration of specific attribute classes is aided by the use of two macros: ENTITY_IS_PROTOTYPE and ATTRIB_FUNCTIONS. Both macros take two arguments, the same two arguments used by MASTER_ATTRIB_DECL: the class and the ACIS library containing the class. ENTITY_IS_PROTOTYPE declares the is_<entity_class>(const ENTITY *) function used to identify the type of an ENTITY. This macro must occur before the class definition, which declares the is_<entity_class>(const ENTITY *) function to be a friend of the attribute class. ATTRIB_FUNCTIONS declares all of the necessary attribute functions, but it does not declare any behavior setting functions; therefore, if you want other than the default behavior for your attribute class, you will need to declare the appropriate behavior setting functions. You will also need to declare the constructors for the attribute class, one of which must be callable with zero arguments. (The restore mechanism calls a constructor with zero arguments. You may choose to declare a special constructor for this purpose, or declare a constructor with default values for all its arguments.) And of course, you will need to declare any member data the attributes contains, as well as member functions to set and get the data members.

The definition of specific attribute classes is slightly more complex than their declaration. The definition of specific attribute classes is aided by the use of several ATTRIB and ENTITY definition macros. The most commonly used macros for attribute definition are listed in the table below.

Macro Description
ATTRIB_DEF Defines how the attribute will be duplicated, lost, destructed, and debugged. The argument to this macro is a string that is printed whenever instances of this class are debugged. Following this macro you should insert code to debug your class-specific data members.
SAVE_DEF Terminates the function for debugging data members and starts the function for saving data members. You should insert code to write your class-specific data members immediately after this macro.
RESTORE_DEF Terminates the function for saving data members and starts the function for restoring data members. You should insert code to read your class-specific data members immediately after this macro.
COPY_DEF Terminates the function for restoring data members and starts the function for copying data members. You should insert code to copy your class-specific data members immediately after this macro.
SCAN_DEF Terminates the function for copying data members and starts the function for scanning or traversing the data structure. You should insert code to scan connected entities immediately after this macro.
FIX_POINTER_DEF Terminates the function for scanning the data structure and starts the function for fixing entity pointers. You should insert code to fix entities pointers immediately after this macro.
TERMINATE_DEF Terminates the macro-based definition of the attribute.

Most likely your application's specific attribute classes will use the above macros in the order they are listed above; however, if one of your attribute classes require specific actions to occur when they are duplicated, lost, or destructed, then you should use the following set of four macros to replace ATTRIB_DEF.

Macro Description
ATTCOPY_DEF Starts the function to duplicate dependent data of the attribute for the attributes (rollback) bulletin. The data members of the attribute will have been duplicated before this function is called. You should insert code to duplicate dependent data immediately after this macro. (For example, if your attribute pointed to a mutable character string, you might want to copy the string.)
LOSE_DEF Terminates the function for duplicating the dependent data of the attribute and starts the function for losing dependent data of the attribute. You should insert code to lose dependent data of the attribute immediately after this macro.
DTOR_DEF Terminates the function for losing the dependent data of the attribute and starts the function for destructing dependent data of the attribute. You should insert code to destruct dependent data of the attribute immediately after this macro.
DEBUG_DEF Terminates the function for destructing the dependent data of the attribute and starts the function for debugging the attribute. You should insert code to debug the attribute immediately after this macro.

For additional information, refer to Attribute Derivation Macros and Entity Derivation Macros.

Simple Attributes

Simple attribute classes do not contain entity pointers so the definition of their member functions for saving, restoring, and copying are simpler. Perhaps the easiest way to describe how to implement a simple attribute class it to provide an example. The following example demonstrates how to create a color attribute class. This class, ATTRIB_COL, is derived from the organization attribute class, ATTRIB_ABC. ATTRIB_COL contains one data member, an integer that represents the color of the owner. In this example we will define one constructor, which takes two arguments but has default values for each so it can be called by the restore mechanism. We will define member functions to get and set the color value. We will specify four behaviors for the class, two using set_action functions and two using the <action>_owner functions. And we will define all the necessary utility functions for debugging, copying, saving, and restoring using macros. Below is the header file for the ATTRIB_COL class. Notice that it: (1) contains the necessary header file(s), (2) contains declares and #defines the type and level, and (3) contains the class definition. We should also mention that embedded within the ATTRIB_FUNCTIONS macro are private:, protected:, and public: statements so everything after the ATTRIB_FUNCTIONS macro is public.

// This is the header file for the ATTRIB_COL class.

#if !defined(ATTRIB_COL_CLASS)
#define ATTRIB_COL_CLASS

#include "at_abc.hxx"

extern int ATTRIB_COL_TYPE;
#define ATTRIB_COL_LEVEL (ATTRIB_ABC_LEVEL + 1)

ENTITY_IS_PROTOTYPE(ATTRIB_COL, NONE)

class ATTRIB_COL: public ATTRIB_ABC {

int color_data;

public:

ATTRIB_COL(ENTITY* = NULL, int = 0);

int color() const {return color_data;}
void set_color(int);

ATTRIB_FUNCTIONS(ATTRIB_COL, NONE);

virtual void merge_owner(ENTITY *, logical);
virtual void replace_owner(ENTITY *, logical);

virtual logical pattern_compatible() const;
};

#endif

Did you notice that the header file contained an in-line function for getting the color value but not one for setting the color value? Why do you think we did that?

...

Whenever the color value is set the entity is changed; therefore, we must be sure that the change is recorded on a bulletin. To do this we must call backup before setting the color value.

The source file for the color attribute begins with inclusion the necessary header files. Following this are the four macros defining the class, its parent class, and the ACIS libraries they are contained within. Following these is the definition of the name of the class used by the save/restore mechanism. In this case the name is "color." This is used when assembling and disassembling the string "color-abc-attrib." Following this we have the seven attribute definition macros with the necessary code in between them. The argument to the ATTRIB_DEF macro is the debug string identifying the attribute class. The code after the ATTRIB_DEF specifies how the data member(s) of the class are debugged.

SAVE_DEF always follows ATTRIB_DEF. The code after SAVE_DEF specifies how the data member(s) of the class are saved. In this case we simply write the integer value of the color to the save file. RESTORE_DEF always follows SAVE_DEF. The code after RESTORE_DEF specifies how the data member(s) of the class are restored. In this case we simply read the integer value from the file and use it to set the color value. COPY_DEF always follows RESTORE_DEF. The code after COPY_DEF specifies how the data member(s) of the class are copied. In this case we simply obtain the integer value from the original instance of the attribute and use it to set the color value in the copied instance of the attribute. SCAN_DEF and FIX_POINTER_DEF always follow COPY_DEF. In this case, because we are not processing entity pointers, we do not add any code following these macros. The block of attribute definition macros is terminated by the TERMINATE_DEF macro.

After the utility functions defined by attribute definition macros we define the functions not defined by the macros, namely the constructor, the color setting function, merge_owner and replace_owner, and pattern_compatible. We will insert calls to set_split_owner_action and set_copy_owner_action into the constructor so that every instance of a color attribute has these behavior set for them. Alternatively, we could have declared and defined split_owner and copy_owner functions. What would the difference in the behavior of the class had been if we had done so? We overload pattern_compatible to allow this attribute to be owned by an entity in a pattern.

Simple Attribute Source File: at_color.cpp
// This is the source file for the ATTRIB_COL class.

#include <stdio.h>
#include "acis.hxx"
#include "datamsc.hxx"
#include "at_color.hxx"

// Implementation of color attribute. This is attached to body,
// face or edge objects, and is used to demonstrate attribute
// migration

// Define macros for this attribute and its parent, to simplify
// later stuff and make it suitable for making into macros.
#define THIS() ATTRIB_COL
#define THIS_LIB NONE
#define PARENT() ATTRIB_ABC
#define PARENT_LIB NONE

// Identifier used externally to identify a particular entity
// type. This is used in the save/restore system for translating
// to/from external file format.
#define ATTRIB_COL_NAME "color"

// Implement the standard attribute functions.
ATTRIB_DEF("color_attribute")
// At this point the application writer places code to write out
// useful information from the attribute (none if he does not want
// to.)

// Define color names for printout
static char* col_name[] = {
"black",
"red",
"green",
"blue",
"cyan",
"yellow",
"magenta",
"white"
};
debug_string("color", col_name [color_data], fp);

SAVE_DEF
// Here the application writer inserts code to write specific
// attribute information to the save file, and to insert any
// pointers into the list.

// Save my specific data
write_int(color_data);

RESTORE_DEF
// Here the application writer inserts code to read specific
// attribute information from the save file.

// Restore my specific data

COPY_DEF
// Here the application writer inserts code to copy data items into
// the new object, using "list" to convert any pointers into indices.

// Copy my specific data
set_color(from->color());

SCAN_DEF
// Here the application writer inserts code to enter any pointers
// into the list.

// (no specific pointer data)

FIX_POINTER_DEF
// Here the application writer inserts code to convert any pointers
// from array indices into array contents, where a negative index
// represents NULL. Special care must be taken with entries pointing
// to objects with use counts, for example, to ensure that the target
// is correctly updated, and that the pointer is not followed until
// it has been converted from the array index.

// (no specific pointer data)

TERMINATE_DEF

// Constructor for a color attribute
ATTRIB_COL::ATTRIB_COL(
ENTITY* owner,
int col
) : ATTRIB_ABC(owner)

{
// Initialize my data members.
color_data = col;

// The following two function calls specify the actions
// of split_owner and copy_owner, rather than implementing
// these functions.

// If my owner is split, create a new instance of myself
// on the copy.
set_split_owner_action(SplitCopy);

// If my owner is copied, create a new instance of myself
// on the copy.
set_copy_owner_action(CopyCopy);
}
// Set the member data.
void ATTRIB_COL::set_color(
int new_col
)
{
// If we are changing the ATTRIB, we must first call backup
// to be sure the change is recorded on a BULLETIN.
backup();
color_data = new_col;
}

// Virtual function called when two entities are to be merged.
void ATTRIB_COL::merge_owner(
ENTITY* other_ent,
logical delete_owner
)
{
// If the owner of this attribute is going to be deleted, and
// there is no color attached to the other entity, then we
// transfer to that other entity.
if (delete_owner) {
ATTRIB* other_att = find_attrib(
other_ent,
ATTRIB_ABC_TYPE,
ATTRIB_COL_TYPE
);
if (other_att == NULL) {
// No color on other entity, so transfer ourself.
move(other_ent);
}
}
}

// Virtual function called during replacement.
void ATTRIB_COL::replace_owner(
ENTITY* other_ent,
logical replace_owner
)
{
// If the owner of this attribute is going to be replaced
// (and hence, deleted), and there is no color attached
// to the other entity, then we transfer to that other
// entity.
if (replace_owner) {
ATTRIB* other_att = find_attrib(
other_ent,
ATTRIB_ABC_TYPE,
ATTRIB_COL_TYPE
);
if (other_att == NULL) {
// No color on other entity, so transfer ourself.
move(other_ent);
}
}
}

// Specify that this attribute is pattern-compatible.
logical ATTRIB_COL::pattern_compatible() const
{
return TRUE;
}

You can easily modify this attribute class for use in your application wherever your attribute classes do not contain any ACIS ENTITY pointers. If you application does require attribute classes that contain ACIS ENTITY pointers, you should make use of the complex attribute class defined in the next subsection.

Complex Attributes

Complex attribute classes contain entity pointers so the definition of their member functions for saving, restoring, and copying are slightly more complex than for simple attribute classes. As with simplex attribute classes perhaps the easiest way to describe how to implement a complex attribute class it to provide an example. The following example demonstrates how to create an attribute class that represents a "blind hole" in a body. To keep this example simple we shall define a blind hole as having a cylindrical side face with two loops and a planar bottom face with a single loop. We shall attach any such attributes to the body so it possesses a list of all such blind holes.

This class, ATTRIB_BLIND_HOLE, is derived from the organization attribute class, ATTRIB_ABC. ATTRIB_BLIND_HOLE contains two data members, entity pointers that represents the side and bottom faces of a blind hole. In this example we will define one constructor, which takes three arguments but has default values for each so it can be called by the restore mechanism. We will define member functions to get and set the side and bottom faces. We will specify four behaviors for the class, all using the <action>_owner functions. And we will define all the necessary utility functions for debugging, copying, saving, and restoring using macros. Below is the header file for the ATTRIB_BLIND_HOLE class. Notice that it: (1) contains the necessary header file(s), (2) contains declares and #defines the type and level, and (3) contains the class definition.

// This is the header file for the ATTRIB_BLIND_HOLE class.

#if !defined(ATTRIB_BLIND_HOLE_CLASS)
#define ATTRIB_BLIND_HOLE_CLASS

#include "at_abc.hxx"
class FACE;

extern int ATTRIB_BLIND_HOLE_TYPE;
#define ATTRIB_BLIND_HOLE_LEVEL (ATTRIB_ABC_LEVEL + 1)

ENTITY_IS_PROTOTYPE(ATTRIB_BLIND_HOLE, NONE)

class ATTRIB_BLIND_HOLE: public ATTRIB_ABC {

// The private data members
FACE * side_face;
FACE * bottom_face;

// Declare the utility functions
ATTRIB_FUNCTIONS(ATTRIB_BLIND_HOLE, NONE);

// The constructor
ATTRIB_BLIND_HOLE(ENTITY* = NULL, FACE* = NULL, FACE* = NULL);

// Data query functions
FACE * get_side_face() const {return side_face;}
FACE * get_bottom_face() const {return bottom_face;}

// Functionality query function
logical moveable() const {return FALSE;}

// Data setting functions
void set_side_face(FACE*);
void set_bottom_face(FACE*);

// Pattern compatibility function
virtual logical pattern_compatible() const;
};

#endif

How does the above header file compare with the header file for the color attribute? What are the primary differences between the two files?

Complex Attribute Source File: at_blind_hole.cpp
// This is the source file for the ATTRIB_BLIND_HOLE class.

#include <stdio.h>
#include "acis.hxx"
#include "datamsc.hxx"
#include "at_blind_hole.hxx"
#include "face.hxx"

// Implementation of the blind hole attribute. This is attached to
// a body and is used to demonstrate complex attribute migration

// Define macros for this attribute and its parent, to simplify
// later stuff and make it suitable for making into macros.
#define THIS() ATTRIB_BLIND_HOLE
#define THIS_LIB NONE
#define PARENT() ATTRIB_ABC
#define PARENT_LIB NONE

// Identifier used externally to identify a particular entity
// type. This is used in the save/restore system for translating
// to/from external file format.
#define ATTRIB_BLIND_HOLE_NAME "blind_hole"

// Implement the standard attribute functions.

ATTRIB_DEF("blind hole attribute")
// At this point the application writer places code to write out
// useful information from the attribute (none if he does not want
// to.)

// Debug my specific data
debug_new_pointer("Side Face", get_side_face(), fp);
debug_new_pointer("Bottom Face", get_bottom_face(), fp);

SAVE_DEF
// Here the application writer inserts code to write specific
// attribute information to the save file, and to insert any
// pointers into the list.

// Save my specific data
write_ptr(get_side_face(), list);
write_ptr(get_bottom_face(), list);

RESTORE_DEF
// Here the application writer inserts code to read specific
// attribute information from the save file.

// Restore my specific data

COPY_DEF
// Here the application writer inserts code to copy data items into
// the new object, using "list" to convert any pointers into indices.

// Copy my specific data
set_side_face((FACE*) list.lookup(from->get_side_face()));
set_bottom_face((FACE*) list.lookup(from->get_bottom_face()));

SCAN_DEF
// Here the application writer inserts code to enter any pointers
// into the list.

// Add my specific entity pointers

FIX_POINTER_DEF
// Here the application writer inserts code to convert any pointers
// from array indices into array contents, where a negative index
// represents NULL. Special care must be taken with entries pointing
// to objects with use counts, for example, to ensure that the target
// is correctly updated, and that the pointer is not followed until
// it has been converted from the array index.

// Fix my specific entity pointers

TERMINATE_DEF

// Constructor for a blind_hole attribute
ATTRIB_BLIND_HOLE::ATTRIB_BLIND_HOLE(
ENTITY* owner,
FACE* sface,
FACE* bface
) : ATTRIB_ABC(owner)

{
// Initialize my data members.
side_face = sface;
bottom_face = bface;

// The following four function calls specify the actions
// of copy_owner, merge_owner, replace_owner, and split_owner
// rather than implementing these functions.

// If my owner is copied, create a new instance of myself
// on the copy.
set_copy_owner_action(CopyCopy);

// If my owner is merged, lose myself.
set_merge_owner_action(MergeLose);

// If my owner is replaced, lose myself.
set_replace_owner_action(ReplaceLose);

// If my owner is split, lose myself.
set_split_owner_action(SplitLose);
}

// Set the member data.
void ATTRIB_BLIND_HOLE::set_side_face(
FACE * sface
)
{
// If we are changing the ATTRIB, we must first call backup
// to be sure the change is recorded on a BULLETIN.
backup();
side_face = sface;
}

void ATTRIB_BLIND_HOLE::set_bottom_face(
FACE * bface
)
{
// If we are changing the ATTRIB, we must first call backup
// to be sure the change is recorded on a BULLETIN.
backup();
bottom_face = bface;
}

// Specify that this attribute is not pattern-compatible.
logical ATTRIB_BLIND_HOLE::pattern_compatible() const
{
return FALSE;
}

How does the above source file compare with the source file for the color attribute? What are the primary differences between the two files?

Bridge Attributes

Bridge attributes contain or point to application data structures. The implementation of bridge attributes is highly application-specific. The declaration and definition of these attributes is similar to the examples we have presented for simple and complex attributes, but they may be more complicated. If a bridge attributes contains application data, it is the responsibility of the application developer to define how the data is saved, restored, copied, and so on. Similarly, if a bridge attributes points to application data, it is the responsibility of the application developer to define what should happen to the pointers to the application data, and more importantly, what should happen to the application data itself.

Example Programs

Testing Our Simple Attribute Class

To test the color attribute class we generate a block, attach a color attribute to block, debug the block to a debug file, and save the block to an SAT file. The initialization and termination functions for this example remain similar to previous examples.

In this example we save the block with its color attribute to a SAT file. Whenever an application generates an SAT (or SAB) file Spatial requests that the application save the units and the name of the application in the header of the SAT (or SAB) file. This is accomplished using api_set_file_info. To make the SAT file more easily human readable we add sequence numbers to the file using the sequence_save_files option. In this case, instead of using one of the option_header member functions to set the option value, we set the value using api_set_int_option. The creation of the SAT file is demonstrated in do_something.

Main Program Source File: main1.cpp
// This file contains the main program.

#include <stdio.h>
#include <stdlib.h>
#include "acis.hxx"
#include "logical.h"
#include "api.hxx"
#include "kernapi.hxx"
#include "lists.hxx"
#include "body.hxx"
#include "position.hxx"
#include "cstrapi.hxx"
#include "debug.hxx"
#include "at_color.hxx"
#include "fileinfo.hxx"

// Declaration of the ACIS licensing function.
void unlock_spatial_products_<NNN>();

// Declaration of our functions.
void do_something();
int my_initialization();
int my_termination();

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and propagate the error.
#define CHECK_RESULT                                     \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
sys_error(err_no);                               \
}

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and return the error number.
#define CHECK_RESULT2                                    \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
return err_no;                                   \
}

// The main program...
int main (int argc, char** argv) {

int ret_val = my_initialization();
if (ret_val)
return 1;

do_something();

ret_val = my_termination();
if (ret_val)
return 1;

printf("Program completed successfully\n\n");
return 0;
}

void do_something(){

BODY * my_block;

API_BEGIN

// Create a solid block.
SPAposition pts[2];
pts[0] = SPAposition(10, 10, 10);
pts[1] = SPAposition(-10, -10, -10);
result = api_solid_block(pts[0], pts[1], my_block);
CHECK_RESULT

// Apply a "red" color attribute to the block.
ACIS_NEW ATTRIB_COL (my_block, 1);

// Write out the debug data file
FILE* fp = fopen("Example.dbg", "w");
debug_entity(my_block, fp);
fclose(fp);

// Write out the "SAT" data file
// Setting the units and product_id
FileInfo fileinfo;
fileinfo.set_units (1.0);
fileinfo.set_product_id ("Example Attribute Application");
result = api_set_file_info((FileId | FileUnits), fileinfo);
CHECK_RESULT

// Also set the option for sequence numbers in the SAT file
result = api_set_int_option("sequence_save_files", 1);
CHECK_RESULT

FILE* save_file = fopen("Example.sat", "w");
ENTITY_LIST slist;
result = api_save_entity_list(save_file, TRUE, slist);
fclose(save_file);
CHECK_RESULT

API_END

// Clean up
api_del_entity(my_block);

return;
}

int my_initialization() {

// Start ACIS.
outcome result = api_start_modeller(0);
CHECK_RESULT2

// Call the licensing function to unlock ACIS.
unlock_spatial_products_<NNN>();

// Initialize all necessary components.
result = api_initialize_kernel();
CHECK_RESULT2

return 0;
}

int my_termination() {
// Terminate all necessary components.
outcome result = api_terminate_kernel();
CHECK_RESULT2

// Stop ACIS and release any allocated memory.
result = api_stop_modeller();
CHECK_RESULT2

return 0;
}

If you open the SAT file generated by this example program in a text editor, does the definition of our color attribute appear similar to what was described in Save and Restore? Can you follow the pointers in the SAT file? The debug file may be helpful the understand the various pointers of the various entities. It is not necessary to understand the save format for the various entity types, but it is useful to understand the types of information stored in the file and how ACIS interprets SAT files. For instance, the SAT file generated by this example program does not contain bounding boxes, because bounding boxes were not generated on the entities before the SAT file was written. If you add a call to get_body_box after creating the block, and rerun the program, you should see bounding boxes in the SAT file.

Testing Our Complex Attribute Class

To test the complex attribute class we generate a block with two blind holes in it, create two attributes that point to the faces of the blind holes and attach them to block, debug the block to a debug file, and save the block to an SAT file. The generation of the debug file, the SAT, and the initialization and termination functions for this example are identical to the previous example. The function to recognize blind holes is not rigorous, but does demonstrate that regions of interest in a model can be located and pointed to by attributes.

Main Program Source File: main2.cpp
// This file contains the main program.

#include <stdio.h>
#include <stdlib.h>
#include "acis.hxx"
#include "logical.h"
#include "api.hxx"
#include "kernapi.hxx"
#include "lists.hxx"
#include "alltop.hxx"
#include "plane.hxx"
#include "cone.hxx"
#include "get_top.hxx"
#include "position.hxx"
#include "transf.hxx"
#include "cstrapi.hxx"
#include "boolapi.hxx"
#include "debug.hxx"
#include "at_blind_hole.hxx"
#include "fileinfo.hxx"

// Declaration of the ACIS licensing function.
void unlock_spatial_products_<NNN>();

// Declaration of our functions.
void do_something();
outcome make_a_body(BODY*&);
outcome identify_blind_holes(BODY*);
int my_initialization();
int my_termination();

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and propagate the error.
#define CHECK_RESULT                                     \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
sys_error(err_no);                               \
}

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and return the error number.
#define CHECK_RESULT2                                    \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
return err_no;                                   \
}

// The main program...
int main (int argc, char** argv) {

int ret_val = my_initialization();
if (ret_val)
return 1;

do_something();

ret_val = my_termination();
if (ret_val)
return 1;

printf("Program completed successfully\n\n");
return 0;
}

void do_something(){

EXCEPTION_BEGIN
BODY* my_body = NULL;
EXCEPTION_TRY

// Create a body with a couple blind holes.
outcome result = make_a_body(my_body);
CHECK_RESULT

// Identify any blind holes.
result = identify_blind_holes(my_body);
CHECK_RESULT

// Write out the debug data file
FILE* fp = fopen("Example.dbg", "w");
debug_entity(my_body, fp);
fclose(fp);

// Write out the "SAT" data file
// Setting the units and product_id
FileInfo fileinfo;
fileinfo.set_units (1.0);
fileinfo.set_product_id ("Example Attribute Application");
result = api_set_file_info((FileId | FileUnits), fileinfo);
CHECK_RESULT

// Also set the option for sequence numbers in the SAT file
result = api_set_int_option("sequence_save_files", 1);
CHECK_RESULT

FILE* save_file = fopen("Example.sat", "w");
ENTITY_LIST slist;
result = api_save_entity_list(save_file, TRUE, slist);
fclose(save_file);
CHECK_RESULT

EXCEPTION_CATCH(TRUE)
// Clean up
if (my_body)
api_del_entity(my_body);
EXCEPTION_END_NO_RESIGNAL

return;
}

outcome make_a_body(BODY*& my_body) {
// Create a solid block with a couple blind holes.
BODY * my_cyl = NULL;
API_BEGIN

SPAposition pts[2];
pts[0] = SPAposition(10, 10, 10);
pts[1] = SPAposition(-10, -10, -10);
result = api_solid_block(pts[0], pts[1], my_body);
CHECK_RESULT

result = api_make_frustum(10.0, 1.0, 1.0, 1.0, my_cyl);
CHECK_RESULT
result = api_apply_transf(my_cyl,
translate_transf(SPAvector(0.0, 0.0, 10.0)));
CHECK_RESULT
result = api_subtract(my_cyl, my_body);
CHECK_RESULT
result = api_make_frustum(10.0, 1.0, 1.0, 1.0, my_cyl);
CHECK_RESULT
result = api_apply_transf(my_cyl,
translate_transf(SPAvector(0.0, 0.0, -10.0)));
CHECK_RESULT
result = api_subtract(my_cyl, my_body);
CHECK_RESULT

API_END

if (!result.ok()) {
if (my_body) {
api_del_entity(my_body);
my_body = NULL;
}
if (my_cyl) {
api_del_entity(my_cyl);
my_cyl = NULL;
}
}

return result;
}

outcome identify_blind_holes(BODY* my_body) {
// Search the body for regions that look like blind holes.
// If any such regions are found, identify them with attributes.

// We could devise much more stringent recognition criteria,
// but for this example we assume a blind hole will have
// a conical side face, with two loops and a planar bottom face,
// with one loop.

API_BEGIN

ENTITY_LIST face_list;
get_faces(my_body, face_list);

for (int i = 0; i < face_list.count(); i++) {

// Search for side faces.
FACE * f = (FACE*) face_list[i];

// The side face should be a cylinder (i.e., cone.)
SURFACE * s = f->geometry();
if (!is_CONE(s))
continue;

// The side face should have two loops.
ENTITY_LIST loop_list;
get_loops(f, loop_list);
if (loop_list.count() != 2)
continue;

// Get a list of all the edges of the two loops.
ENTITY_LIST edge_list;
get_edges((LOOP*)loop_list[0], edge_list);
get_edges((LOOP*)loop_list[1], edge_list);

// Get a list of all the faces of the edges.
ENTITY_LIST face_list2;
for (int j = 0; j < edge_list.count(); j++)
get_faces((EDGE*)edge_list[j], face_list2);

// See if any of these faces meet the criteria for a side face.
FACE * bface = NULL;
for (int k = 0; k < face_list2.count(); k++) {
FACE * f2 = (FACE*)face_list2[k];
if (f2 == f)
continue;
SURFACE * s2 = f2->geometry();
if (!is_PLANE(s2))
continue;
ENTITY_LIST loop_list2;
get_loops(f2, loop_list2);
if (loop_list2.count() != 1)
continue;

// If we reached this point we found a good bottom face
// which implies our side was good too.
bface = f2;
break;
}

if (bface)
ACIS_NEW ATTRIB_BLIND_HOLE(my_body, f, bface);

}

API_END

return result;
}

int my_initialization() {

// Start ACIS.
outcome result = api_start_modeller(0);
CHECK_RESULT2

// Call the licensing function to unlock ACIS.
unlock_spatial_products_<NNN>();

// Initialize all necessary components.
result = api_initialize_kernel();
CHECK_RESULT2

return 0;
}

int my_termination() {
// Terminate all necessary components.
outcome result = api_terminate_kernel();
CHECK_RESULT2

// Stop ACIS and release any allocated memory.
result = api_stop_modeller();
CHECK_RESULT2

return 0;
}

A debug file for the body created by the second test program is located here. Do you see how the debug information for the blind hole attributes was generated? The information about the ENTITY (the BULLETIN pointer and the ATTRIB pointer) was generated by the ENTITY class, the information about the ATTRIB (the owner pointer and the previous and next ATTRIB pointers) was generated by the ATTRIB class, and the information about each blind_hole attribute (the side and bottom face pointers) was generated by our ATTRIB_BLIND_HOLE class. (By default the pointer values in a debug file are relative addresses. If you would prefer to use absolute addresses in your debug files, you can specify this using the debug_absolute_addresses option.) Can you find the same information (minus the BULLETIN pointer) in the SAT file generated by test program?

Neither of these example programs demonstrate what happens to the attributes if the owner of the attributes is modified. It is left as an exercise for the reader to perform an operation of the owner of the attributes and verify what happens to the attributes.

Related Topics

While we are on the subject of connecting ACIS with your application we should introduce another mechanism for tightening the connection between ACIS and your application. There are a number of ways in which an application can learned about a change in the ACIS model. One method that we described in the history tutorial was to examine the history stream after every operation. This allows you to examine every entity that has been created, changed, or deleted during the operation. In this tutorial we have described the attribute notification methods. Using these methods your application can be aware of changes made to any ACIS entities to which your attributes have been attached. A third mechanism for learning of changes to the ACIS data structure is through the use of annotations and tags. Feature Naming and Annotations describes how annotations can be used to learn about changes to the ACIS data structure. Although the intent of that article is to describe the use of annotations to support feature naming, it also provides a good overview of annotations.

Attributes provide a mechanism through which application-specific data can be attached to entities; however, they will not allow you to attach virtual methods to entities. Virtual methods with run time binding can be added to entity classes using run-time virtual methods which uses ENTITY::add_method and ENTITY::call_method. Run-time virtual methods are beyond the scope of this tutorial, but you should be aware of their existence. In practice, run-time virtual methods are not that commonly used. Typically applications contain functions that use switch or if-else-if logic to take appropriate action for various ACIS entity types.

Creating New ENTITY Classes

There are times when it is more appropriate to derive a new entity class than to derive a new attribute class. Attributes typically augment the data of existing entities, whereas an entity can stand alone. For example, the "blind hole" attribute we derived as an example of a complex attribute should most likely be a stand alone entity rather than an attribute attached to the body.

Much of what we have said about creating new attribute classes applies to creating new entity classes. After all, an attribute is a type of entity and much of their functionality is derived from the ENTITY class. The primary differences lie in the macros used to declare and define new entities and attributes.

Any new entity classes you create should be derived from an organization entity class. This class would be called ENTITY_<your_sentinel>. Similar to creating an organization attribute class, an organization entity class is created using macros; however, instead of using MASTER_ATTRIB_DECL and MASTER_ATTRIB_DEFN you would use MASTER_ENTITY_DECL and MASTER_ENTITY_DEFN. The specific entity classes are declared using the ENTITY_FUNCTIONS macro instead of the ATTRIB_FUNCTIONS macro. The definition of specific entity classes is very similar to the definition of specific attribute classes classes, except you will typically use ENTITY_DEF instead of ATTRIB_DEF. Unlike attributes, entities do not need the functions to specify their behavior when their owner changes. The style of the header and source files for entities is identical to the style used for attributes.

The reason we said that "you will typically use ENTITY_DEF instead of ATTRIB_DEF" above is because entities have a much wider range of use than attributes; therefore, one needs more flexibility in their definition. If you were to look in entity.hxx you would see there are many more macros for the definition of entities than we have described in our discussions regarding attributes. We have listed some of the more commonly used macros for entity definitions below. Some of these require additional declarations in the entity's header file. For those cases we have listed the associated declaration macro.

Declaration Macro Definition Macro Description
FIXUP_COPY_DEF none Defines fixup_copy. Required for all entities - unless SIMPLE_COPY_LOSE is used.
SIMPLE_COPY_LOSE none Defines fixup_copy, lose, and the destructor for classes that do not have special requirements for updating connected objects or any dependent data structures. Otherwise you must define your own functions.
COPY_WITH_DEEP_COPY_DEF none Replaces COPY_DEF if "deep" rather than "shallow" copies are to be permitted.
FULLSIZE_DEF FULLSIZE_FUNCTION Implements full_size, which counts the size of all things this class owns. After this macro you must add code that counts the size of all things this class owns.
TRANSFORM_DEF TRANSFORM_FUNCTION Defines (non-default) actions to be taken when transformations are applied. After this macro you must add code to transform the entity and an include a return statement with the value TRUE.
LOOKUP_DEF LOOKUP_FUNCTION Determines whether a class will have its own debug list or be part of its parent's debug list.

We did not mention FIXUP_COPY_DEF or SIMPLE_COPY_LOSE when discussing attributes because their functionality is incorporated into ATTRIB_DEF. We did not mention COPY_WITH_DEEP_COPY_DEF because typically attributes do not need to be deep copied. For more information on deriving your own entity classes, refer to Entity Derivation Macros and Deriving Classes.

As an exercise to derive an ENTITY class (instead of an ATTRIB class) you might want to convert the blind hole attribute class into a entity class. Perhaps you could define an entity class called FEATURE and derive several classes (such as "BLIND_HOLE" or "THRU_HOLE") from the FEATURE class. The main program would keep a list of the features found in the model. How would you invalidate a feature if one of its faces was modified? How would you capture mating faces on two different bodies? Do you see how attributes and entities can work together?

Tutorial 12: Importing and Exporting Models

Introduction

The preferred means to import non-ACIS models into ACIS and to export ACIS models into non-native file formats is to use Spatial's Interoperability component (InterOp.) The recommended interface for InterOp is the "InterOp Connect" interface. InterOp can do much more than translate models into and out of ACIS, and InterOp has other interfaces, but this tutorial shall focus on these import and export capabilities and the InterOp Connect interface. The most common file formats supported by InterOp are listed below.

• CATIA V4
• CATIA V5
• IGES
• Parasolid
• STEP
• VDA-FS

Whenever you translate models from one geometric modeling system into another system there may be validity issues. There are a variety of reasons for these issues, but the most common causes are: differences in model tolerances, differences in model representations, and if one is using a file as the data exchange medium, the limitations of the exchange file format.

Different geometric modeling systems use different default tolerances. When a non-ACIS based application generates a file containing a representation of a geometric model, the model in the file reflects the tolerances used by the non-ACIS based application. Most tolerance-related problems result from a difference in the positional tolerances of the exporting and importing systems. If the positional tolerance in the exporting application is looser than the positional tolerance in importing application, small gaps may occur in the model. If the positional tolerance in the exporting application is tighter than the positional tolerance in the importing application, small features (for example, short edges and narrow faces) may become lost in the model. In addition to the tolerance related issues, differences in data structures between the exporting and importing systems, and limitations of the exchange file format may cause translation problems. Spatial suggests a "Healing Workflow" to overcome these issues.

Using InterOp Connect

Before starting this tutorial, we would suggest that you familiarize yourself with the InterOp Connect interface and how the interface works.

Importing Models

InterOp Connect will translate a non-native formatted file containing a non-native data structure into an ACIS model. This is accomplished by specifying a SPAIDocument as the source and a SPAIAcisDocument as the destination. This informs InterOp Connect that the input will be a formatted file and the output will be ACIS ENTITYs. The format of the source file is determined by the file's suffix. If the suffix of the source file is non-standard, the format type can be specified using SPAIAcisDocument::SetType(const char* ipszType). Standard file suffixes and valid string arguments for SPAIAcisDocument::SetType(const char* ipszType) are described in SPAIDocument.

When the new ACIS ENTITYs are constructed they belong to the SPAIAcisDocument. This implies they will be destructed when the SPAIAcisDocument is destructed. To prevent this, you must detach the ACIS ENTITYs from the SPAIAcisDocument before the SPAIAcisDocument is destructed. This is accomplished by calling SPAIAcisDocument::DetachEntities.

A function demonstrating how to convert a non-native formatted file into ACIS ENTITYs is shown below.

void import_from_file(
const char * file_name,  // (in)  Name of the source file
ENTITY_LIST *& my_list   // (out) List of top-level ENTITYs
) {
SPAIDocument src(file_name);
SPAIAcisDocument dst;
SPAIConverter converter;
converter.Convert(src, dst);
dst.GetEntities(my_list);
dst.DetachEntities();
}

Note: The ENTITY_LIST in the above function is constructed on the heap and must be destructed by the calling function, for example, ACIS_DELETE my_list;.

Exporting Models

In addition to importing non-native formatted files, InterOp Connect also translates an ACIS model into a non-native formatted file. This is accomplished by specifying a SPAIAcisDocument as the source and a SPAIDocument as the destination. This informs InterOp Connect that the input will be ACIS ENTITYs and the output will be a non-native formatted file. The format of the destination file is determined by either the file's suffix or SPAIAcisDocument::SetType(const char* ipszType) as described in the previous section.

A function demonstrating how to convert an ACIS BODY into a non-native formatted file is shown below.

void export_to_file(
BODY * my_body,         // (in)  The ACIS BODY
const char * file_name  // (out) Name of the destination file
) {
ENTITY_LIST acis_ents;
SPAIAcisDocument src(&acis_ents);
SPAIDocument dst(file_name);
SPAIConverter converter;
converter.Convert(src, dst);
}

C++ Example

This example program demonstrates how to import and export IGES and STEP format files. Importing and exporting other files formats is very similar. The main function and the initialization and termination functions are virtually identical to the examples in previous tutorials. The primary differences in the program are: the header files included, do_something, and the functions for import and export. To support InterOp Connect, we have added the following header files.

#include "SPAIDocument.h"
#include "SPAIAcisDocument.h"
#include "SPAIConverter.h"
#include "SPAIFile.h"
#include "SPAIOptions.h"
#include "SPAIOptionName.h"
#include "SPAIResult.h"
#include "SPAIValue.h"
#include "SPAIUnit.h"

do_something generates a solid block, exports this block to IGES and STEP format files, imports the IGES and STEP format files, and compares the resulting models with the original block. Importing the model from both IGES and STEP format files is performed by import_from_file. This function is identical to the one presented in Importing Models except we examine and return the result of the conversion process in case there is an error. Exporting the model to both IGES and STEP format files is performed by export_to_file. This function is identical to the one presented in Exporting Models except we examine and return the result of the conversion process in case an error occurs.

The comparison of the original model with the models resulting from exporting and importing is very rudimentary. We simply look at the top level topological ENTITYs and what is beneath them. We use my_debug_list to debug a list of ENTITYs. For each top level ENTITY we determine the number and types of dependent ENTITYs. This summation is performed by debug_size.

Building an ACIS application which includes InterOp Connect is slightly different from our previously described build procedures and, once again, depends upon the platform upon which your application resides. Revised build procedures are provided for Microsoft's Visual Studio and Linux. In addition to these descriptions, InterOp Samples provides Visual Studio projects and Unix makefiles that may be of assistance.

C++ Example
#include <stdio.h>

#include "acis.hxx"
#include "api.hxx"
#include "kernapi.hxx"
#include "cstrapi.hxx"
#include "intrapi.hxx"
#include "lists.hxx"
#include "alltop.hxx"
#include "get_top.hxx"
#include "debug.hxx"

#include "SPAIDocument.h"
#include "SPAIAcisDocument.h"
#include "SPAIConverter.h"
#include "SPAIFile.h"
#include "SPAIOptions.h"
#include "SPAIOptionName.h"
#include "SPAIResult.h"
#include "SPAIValue.h"
#include "SPAIUnit.h"

// Declaration of the ACIS licensing function.
void unlock_spatial_products_<NNN>();

// Declaration of our functions.
void do_something();
int export_to_file(BODY*, const char *);
int import_from_file(ENTITY_LIST*&, const char *);
void my_debug_list(ENTITY_LIST*);
int my_initialization();
int my_termination();

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and propagate the error.
#define CHECK_RESULT                                     \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
sys_error(err_no);                               \
}

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and return the error number.
#define PROCESS_RESULT                                   \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
return err_no;                                   \
}

// The main program...
int main (int argc, char** argv) {

int ret_val = my_initialization();
if (ret_val)
return 1;

do_something();

ret_val = my_termination();
if (ret_val)
return 1;

printf ("Program completed successfully\n\n");

return 0;
}

void do_something(){

API_BEGIN

// Create a block.
BODY * my_body;
result = api_make_cuboid (20.0, 20.0, 20.0, my_body);
CHECK_RESULT

// Print a summary of the entity and its dependent entities.
printf("The initial block:\n");
ENTITY_LIST initial_list;
my_debug_list(&initial_list);

// Export the block to an IGES format file.
const char iges_file_name[] = "c:\\dummy.igs";
export_to_file(my_body, iges_file_name);

// Export the block to a STEP format file.
const char step_file_name[] = "c:\\dummy.stp";
export_to_file(my_body, step_file_name);

// Delete the initial BODY.
result = api_delent (my_body);
CHECK_RESULT

// Import the data from the IGES format file.
ENTITY_LIST * my_list = NULL;
import_from_file(my_list, iges_file_name);

// Determine what was imported.
printf("The IGES model contains %d entities\n",
my_list->count());
my_debug_list(my_list);

// Delete the model from the IGES format file.
api_del_entity_list(*my_list);

// Delete the list. (It was created on the heap.)
ACIS_DELETE my_list;

// Import the data from the STEP format file.
my_list = NULL;
import_from_file(my_list, step_file_name);

// Determine what was imported.
printf("The STEP model contains %d entities\n",
my_list->count());
my_debug_list(my_list);

// Delete the model from the STEP format file.
api_del_entity_list(*my_list);

// Delete the list. (It was created on the heap.)
ACIS_DELETE my_list;

API_END

return;
}

int export_to_file(
BODY * my_body,
const char * file_name
) {
// This function exports an ACIS body to a file, whose format
// is specified by the suffix on the file name.

ENTITY_LIST acis_ents;
SPAIAcisDocument src(&acis_ents);
SPAIDocument dst(file_name);
SPAIConverter converter;
SPAIResult result = converter.Convert(src, dst);
if (result.GetNumber() != SPAX_S_OK)
printf("InterOp ERROR: %s\n", result.GetMessage());
return result.GetNumber();
}

int import_from_file(
ENTITY_LIST *& my_list,
const char * file_name
) {
// This function imports an ACIS model from a file, whose format
// is specified by the suffix on the file name. The top-level
// entities of the imported model are returned in the ENTITY_LIST.
// The ENTITY_LIST must be deleted by the calling function.
// The DetachEntities method causes InterOp to release ownership
// of the newly created entities. Without this call the new entities
// would be deleted when "dst" goes out of scope.

SPAIDocument src(file_name);
SPAIAcisDocument dst;
SPAIConverter converter;
SPAIResult result = converter.Convert(src, dst);
dst.GetEntities(my_list);
dst.DetachEntities();
if (result.GetNumber() != SPAX_S_OK)
printf("InterOp ERROR: %s\n", result.GetMessage());
return result.GetNumber();
}

void my_debug_list(ENTITY_LIST * my_list) {
// Print a summary of each entity and its dependent entities.
int num_ents = my_list->count();
for (int i = 0; i < num_ents; i++) {
ENTITY * ent = (*my_list)[i];
printf("ENTITY[%d]:\n", i);
debug_size(ent, stdout);
SPAposition min_pt;
SPAposition max_pt;
api_get_entity_box(ent, min_pt, max_pt);
printf("Box: from (%.1f, %.1f, %.1f) to (%.1f, %.1f, %.1f)\n\n",
min_pt.x(), min_pt.y(), min_pt.z(),
max_pt.x(), max_pt.y(), max_pt.z());
}
}

int my_initialization() {

// Start ACIS.
outcome result = api_start_modeller(0);
PROCESS_RESULT

// Call the licensing function to unlock ACIS.
unlock_spatial_products_<NNN>();

// Initialize all necessary components.
result = api_initialize_constructors();
PROCESS_RESULT

return 0;
}

int my_termination() {
// Terminate all necessary components.
outcome result = api_terminate_constructors();
PROCESS_RESULT

// Stop ACIS and release any allocated memory.
result = api_stop_modeller();
PROCESS_RESULT

return 0;
}

Discussion

The results of the "loop tests" may be surprising to some ACIS developers. (A "loop test" is performed by generating a known model, exporting it to a given file format, importing it back into the originating system, and comparing the results with the original model.) With the STEP translations, we started with a block with six faces and ended up with a block with six faces. With the IGES translations, we started with the same block, but ended up with six, disconnected faces. This points out a limitation of IGES formatted files: the resulting models need to be stitched together after they have been imported. We will discuss this stitching process shortly in Using the Healing Workflow but first let's explore a bit more about the translation process.

One of the things you might want to do when exporting or importing a model to or from a file is to generate a log file which details what was done during the translation. This is accomplished using the InterOp Connect interface by opening a file, setting it as the log file, performing the conversion, and closing the log file. A code snippet demonstrating this is shown below.

SPAIFile log_file("c:\\log.txt");
SPAIConverter converter;
converter.StartLog(log_file);
converter.Convert(src, dst);
converter.StopLog(log_file);

Another thing you might want to do when exporting or importing a model to or from a file is to set the units of the source and or destination. When you are exporting a model you may want to specify what the units of the ACIS model are because ACIS is unit-less. If you want the units in the exported file to be different than model units, you should specify what the units of the exported file should be. The converter will scale the dimensions of the model as it converts the model. When you are importing a file the units are specified in the file; however, you may want to specify what the ACIS model units are so the model will be scaled appropriately during the conversion. Specifying the units of the source or destination is accomplished by calling SPAIDocument::SetUnit(const SPAIUnit&). A code snippet demonstrating the setting of the source and destination units is shown below. This would cause all the dimensions in ACIS model to be scaled by a factor of 25.4 in the exported file and for the units of the exported file to be set to millimeters. Additional information on setting and querying file units can be found at Using Units.

SPAIUnit src_unit(SPAIUnitInch);
src.SetUnit(src_unit);
SPAIUnit dst_unit(SPAIUnitMillimeter);
dst.SetUnit(dst_unit);

In addition to specifying the units of the source or destination, there are several options that can be set which control the conversion process. These options are instances of the SPAIOptions class. A number of these options that are common to many of the InterOp translators are described on Common Options. For instance, it is suggested that the Representation option be set to BRep+Assembly for the conversions where it is not possible to determine whether the input file contains BREP, Assembly, or both types of data. You should use this combination when importing an IGES, NX, STEP, or Parasolid file, if you do not know the contents of the file. In addition, there are options that are specific to one particular file format. For instance, options for importing STEP files are enumerated on Options for STEP Reader. Options are set by creating an instance of the SPAIOptions class, setting the desired option values, and telling the SPAIDocument to use these options with SPAIDocument::SetOptions. A code snippet demonstrating this is shown below.

SPAIOptions options;
SPAIValue representation("BRep+Assembly");
SPAIConverter converter;
converter.SetOptions(options);

One of the options common to all the conversion processes is Healing. The Healing option allows you to turn on or off InterOp healing during the conversion process. InterOp healing will correct geometrical and topological problems in the model and create tolerant entities wherever necessary. It is recommended that you leave the Healing option set to TRUE, its default value. The only cases in which you might want to set this option to FALSE are:

• If you know there were no tolerance-related, geometrical, or topological problems in the model, or
• If your application contains healing functionality specific to the types of problems expected in the imported files.

Modified C++ Example

The previous example program has been modified to demonstrate how you can generate a log file, set the units for the source and destination upon export, and set conversion options.

Modified C++ Example
#include <stdio.h>

#include "acis.hxx"
#include "api.hxx"
#include "kernapi.hxx"
#include "cstrapi.hxx"
#include "intrapi.hxx"
#include "lists.hxx"
#include "alltop.hxx"
#include "get_top.hxx"
#include "debug.hxx"

#include "SPAIDocument.h"
#include "SPAIAcisDocument.h"
#include "SPAIConverter.h"
#include "SPAIFile.h"
#include "SPAIOptions.h"
#include "SPAIOptionName.h"
#include "SPAIResult.h"
#include "SPAIValue.h"
#include "SPAIUnit.h"

// Declaration of the ACIS licensing function.
void unlock_spatial_products_<NNN>();

// Declaration of our functions.
void do_something();
int export_to_file(BODY*, const char *);
int import_from_file(ENTITY_LIST*&, const char *);
void my_debug_list(ENTITY_LIST*);
int my_initialization();
int my_termination();

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and propagate the error.
#define CHECK_RESULT                                     \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
sys_error(err_no);                               \
}

// Define a macro to check the outcome of API functions.
// In the event of an error this macro will
// print an error message and return the error number.
#define PROCESS_RESULT                                   \
if (!result.ok()) {                                  \
err_mess_type err_no = result.error_number();    \
printf("ACIS ERROR %d: %s\n",                    \
err_no, find_err_mess(err_no));              \
return err_no;                                   \
}

// The main program...
int main (int argc, char** argv) {

int ret_val = my_initialization();
if (ret_val)
return 1;

do_something();

ret_val = my_termination();
if (ret_val)
return 1;

printf ("Program completed successfully\n\n");

return 0;
}

void do_something(){

API_BEGIN

// Create a block.
BODY * my_body;
result = api_make_cuboid(20.0, 20.0, 20.0, my_body);
CHECK_RESULT

// Print a summary of the entity and its dependent entities.
printf("The initial block:\n");
ENTITY_LIST initial_list;
my_debug_list(&initial_list);

// Export the block to an IGES format file.
const char iges_file_name[] = "c:\\dummy.igs";
export_to_file(my_body, iges_file_name);

// Delete the initial BODY.
result = api_delent(my_body);
CHECK_RESULT

// Import the data from the IGES format file.
ENTITY_LIST * my_list = NULL;
import_from_file(my_list, iges_file_name);

// Determine what was imported.
printf("The IGES model contains %d entities\n",
my_list->count());
my_debug_list(my_list);

// Delete the model from the IGES format file.
api_del_entity_list(*my_list);

// Delete the list. (It was created on the heap.)
ACIS_DELETE my_list;

API_END

return;
}

int export_to_file(
BODY * my_body,
const char * file_name
) {
// This function exports an ACIS body to a file, whose format
// is specified by the suffix on the file name.

ENTITY_LIST acis_ents;
SPAIAcisDocument src(&acis_ents);
SPAIDocument dst(file_name);

// Specify that the original model is in Inches,
// but the resulting file should be in Millimeters.
// The model will be appropriately scaled in the file.

SPAIUnit src_unit(SPAIUnitInch);
src.SetUnit(src_unit);
SPAIUnit dst_unit(SPAIUnitMillimeter);
dst.SetUnit(dst_unit);

SPAIResult result = SPAX_S_OK;
SPAIFile log_file("c:\\export_log.txt");
SPAIConverter converter;
converter.StartLog(log_file);
result = converter.Convert(src, dst);
converter.StopLog(log_file);
if (result.GetNumber() != SPAX_S_OK)
printf("InterOp ERROR: %s\n", result.GetMessage());
return result.GetNumber();
}

int import_from_file(
ENTITY_LIST *& my_list,
const char * file_name
) {
// This function imports an ACIS model from a file, whose format
// is specified by the suffix on the file name. The top-level
// entities of the imported model are returned in the ENTITY_LIST.
// The ENTITY_LIST must be deleted by the calling function.
// The DetachEntities method causes InterOp to release ownership
// of the newly created entities. Without this call the new entities
// would be deleted when "dst" goes out of scope.

SPAIDocument src(file_name);
SPAIAcisDocument dst;

// Query the unit information from the file header.

SPAIValue unit_value;
info.GetUnits(unit_value);
printf("Input Document Unit: %s\n\n", (const char*)unit_value);

// Set the Representation option to Brep and Assembly.

SPAIOptions options;
SPAIValue representation("BRep+Assembly");

SPAIResult result = SPAX_S_OK;
SPAIFile log_file("c:\\import_log.txt");
SPAIConverter converter;
converter.StartLog(log_file);
converter.SetOptions(options);
result = converter.Convert(src, dst);
converter.StopLog(log_file);
dst.GetEntities(my_list);
dst.DetachEntities();
if (result.GetNumber() != SPAX_S_OK)
printf("InterOp ERROR: %s\n", result.GetMessage());
return result.GetNumber();
}

void my_debug_list(ENTITY_LIST * my_list) {
// Print a summary of each entity and its dependent entities.
int num_ents = my_list->count();
for (int i = 0; i < num_ents; i++) {
ENTITY * ent = (*my_list)[i];
printf("ENTITY[%d]:\n", i);
debug_size(ent, stdout);
SPAposition min_pt;
SPAposition max_pt;
api_get_entity_box(ent, min_pt, max_pt);
printf("Box: from (%.1f, %.1f, %.1f) to (%.1f, %.1f, %.1f)\n\n",
min_pt.x(), min_pt.y(), min_pt.z(),
max_pt.x(), max_pt.y(), max_pt.z());
}
}

int my_initialization() {

// Start ACIS.
outcome result = api_start_modeller(0);
PROCESS_RESULT

// Call the licensing function to unlock ACIS.
unlock_spatial_products_<NNN>();

// Initialize all necessary components.
result = api_initialize_constructors();
PROCESS_RESULT

return 0;
}

int my_termination() {
// Terminate all necessary components.
outcome result = api_terminate_constructors();
PROCESS_RESULT

// Stop ACIS and release any allocated memory.
result = api_stop_modeller();
PROCESS_RESULT

return 0;
}

Using the Healing Workflow

As we mentioned in the Introduction, whenever models are imported from non-native formatted files, there may be model integrity issues. InterOp and ACIS offer healing functionality to improve imported models for subsequent use. The "best" combination of healing operations depends upon the intended use of the model and its source.

If the imported model is intended strictly for viewing, you can generally perform the conversion using InterOp healing as we described in the previous section and not perform any subsequent healing operations. If very basic analysis (such as measurement operations) will be required, and you suspect that the model has not been properly stitched into a solid body (as we observed when we imported IGES files in our C++ Example), then stitching is generally recommended. If the model is imported from an application-specific formatted file (such as Pro/E or CATIA V4), gap tightening may be the only healing operation needed. For full use of the model after import, including complex model modifications such as Booleans and Local Operations, post-processing the imported model with Stitching, Simplification, and Gap Tightening may be required. This set of operations prepare the model for intensive ACIS calculations and are referred to as the Recommended Healing Workflow.

The recommended healing workflow will not heal models with significant topological or geometrical inconsistencies. It assumes that imported models had valid representations in some geometric modeling system. Moreover, the healing workflow is not intended to correct all check errors, but rather it is designed to make imported models functional in ACIS.

Stitching

Some file formats do not maintain solid model data structures. Instead, the models are represented as a set of faces or trimmed surfaces. In order to use certain ACIS functionality (such a Boolean or blending operation) the faces of these models must be stitched together to form solid bodies. This stitching is performed by api_stitch. The use of api_stitch in the recommended healing workflow is described here. In addition to using api_stitch, you may need to use api_stitch_nonmanifold if more than two faces meet at an edge. For more information on the stitching algorithm, refer to Tolerant Stitching.

Simplification

Some file formats do not maintain analytic curves and surfaces and as a result analytic curves and surfaces are converted into b-spline geometry. If an imported model contains b-spline geometry that is within (a user specified) tolerance of an analytic curve or surface, the b-spline geometry can be replaced by analytic geometry. This process is referred to as "simplification" and is implemented in api_simplify_entity. The use of api_simplify_entity in the recommended healing workflow is described here. It should be noted that simplification may create gaps in the model that must be resolved by tolerant modeling or gap tightening. Additional information on simplification can be found at Geometry Simplification.

Gap Tightening

Gaps in ACIS models are represented by tolerant entities. Larger gaps (in other words, larger tolerances on tolerant entities) make ACIS operations more difficult. If the size of the gaps can be reduced, subsequent ACIS operations can be performed more efficiently and robustly. Therefore, we recommend that you use "gap tightening" if you plan to perform complex operations on the imported model. Among other things, gap tightening extends and re-intersects the surfaces underlying faces to generate new intersection curves, so this process can be computationally expensive. Gap tightening is implemented in api_tighten_gaps and its use is described here. Additional information on the gap tightening algorithm is available at Tightening Gaps.

Tolerances for the Recommended Healing Workflow

Each of the operations in the Recommended Healing Workflow accept a user-specified tolerance value: tolerant stitching is performed to a user-specified tolerance, geometric simplification is performed to a user-specified tolerance, and gaps are tightened to a user-specified tolerance. But how do you determine an appropriate tolerance value?

The appropriate tolerance values for the three operations may be different. Just as the choice of which operations in the recommended healing workflow depend upon the intended use of the model and its source, so do the decisions surrounding the choices of tolerances. In fact, there are three criteria that should be considered when choosing the tolerances.

• An application may have application-specific tolerances that must be met; for instance, a manufacturing system will have tolerances associated with various manufacturing processes. The tolerances on the ACIS models should be at least an order of magnitude tighter than any manufacturing tolerances used.
• The tolerance of the originating system provides a good estimate for the tolerance needed for stitching. If the tolerance of the originating system is looser than the application's required tolerance, then gap tightening is necessary.
• The minimum feature size in a model provides an upper limit of the tolerance sizes. Ideally you would like the tolerances in the model to be at least an order of magnitude smaller than the smallest feature size. This implies that the tolerance values used during stitching, simplification, and gap tightening should all be at least an order of magnitude tighter than the minimum feature size.

Failsafe Behavior

Each of the API functions used in the recommended healing workflow (api_stitch, api_stitch_nonmanifold, api_simplify_entity, and api_tighten_gaps) can have Failsafe Behavior. When an API function is failsafe, it will attempt to do as much as possible and not fail, even when it encounters errors during the operation. Typically, such operations are organized as a series of atomic operations. If one atomic operation fails, any changes that have been made to the model during that atomic operation are undone (using an API_TRIAL_BEGIN/END block), information about the failure is recorded, and the next atomic operation is attempted. Thus, an API function that is behaving in a failsafe manner may report a partial success. Failsafe behavior is toggled using the careful option. If the careful option is TRUE, a failsafe API will function in a non-failsafe manner, failing if it encounters an error. If the careful option is FALSE, a failsafe API will function in a failsafe manner; that is, if it encounters an error, it records the error information and continues with the operation. When operating on a set of entities it is often preferable to obtain partial success rather than complete failure. For example, if you are simplifying the geometry of a complex model it most likely would be preferable to simplify some of the curves and surfaces, even if a few could not be simplified, rather than not simplifying any of the curves and surfaces.

We shall demonstrate the failsafe behavior of api_stitch in a small program. This program is not intended to demonstrate the full healing workflow or all of the details of stitching. In this program we import an IGES file containing the faces of two hemispherical solids. The IGES file for this example is located here. Each hemispherical solid is represented in the IGES file by three faces. The alignment of the faces of the two hemispheres is shown in the figure below. After importing the six faces, we use api_stitch to stitch the faces together.

Six Faces to Be Stitched

If api_stitch detects a pair of coincident faces, it will not stitch them together. It considers this to be an error condition. The action taken by api_stitch when it encounters coincident faces depends on the value of the careful option and the coincident face handling mode specified by the tolerant_stitch_options or edge_tolstitch_options object passed to this API. There are three coincident face handling modes: SPASTITCH_COIN_SKIP, SPASTITCH_COIN_STITCH, and SPASTITCH_COIN_ERROR. The behavior of this API in both failsafe and non-failsafe modes in conjunction with these coincident face handling modes is described in the Coincident Face Detection and the Failsafe Behavior sections of the library reference description of this API.

The example program demonstrates the behavior of api_stitch under six conditions. It demonstrates the failsafe and non-failsafe behavior of the function in each of the three coincident face handling modes. Moreover, the program is designed to allow simple modification of the order in which the faces are stitched. It turns out the results of stitching depend upon the order in which the faces are stitched, as we shall discuss below. do_something iterates through the six cases, calling process_one_case for each. process_one_case imports the six faces from the IGES file, stitches them together, and examines the outcome and the results of the operation.

In this example program, we examine the contents of the outcome more carefully than we have done in earlier examples. api_stitch records information about error conditions it encountered in the error_info objects that are owned by the outcome; therefore, we examine the error_info objects in each case to see what information they contain. In addition, the tolerant_stitch_options object may contain information about coincident faces, so we examine it too.

If the order of the faces in the IGES file is unaltered, each of the six cases will succeed. In each case api_stitch produces two hemispherical bodies, each containing three faces. If the order of the faces is altered so that the four partial spherical faces are stitched first, followed by the two planar faces, then api_stitch recognizes the coincident faces. Operating in its non-failsafe mode api_stitch will fail, regardless of the coincident face handling mode. Operating in its failsafe mode the behavior of api_stitch depends upon the coincident face handling mode. If the coincident face handling mode is SPASTITCH_COIN_ERROR, api_stitch will fail. If the coincident face handling mode is SPASTITCH_COIN_SKIP or SPASTITCH_COIN_STITCH, api_stitch will succeed, but it will record the coincident face information and the error condition. The models resulting from these two cases are slightly different, as you can see in the output file generated by the program.

Healing Workflow C++ Example

In this example, we import an IGES file and use the recommended healing workflow to make the model functional in ACIS. The original ACIS model contained two bodies, each the frustum of a cone, aligned so that their bases were coincident. To this we added a sheet body, consisting of a single planar face, which would act as a parting face. This model would exercise the manifold and non-manifold stitching algorithms, but it would not not require tolerant stitching, simplification, or gap tightening. So, just to make things interesting, we replaced the curves and surfaces of one of the frustums with loosely fit B-spline approximations. We then united everything and converted the resulting model into an IGES file. The model in the IGES file contains seven faces and is shown below. Note that when the conical face was converted into the loosely fitting B-spline, it was split into two faces, and when the other conical face was exported to and imported from IGES, it was converted into two surfaces of revolution. A copy of this IGES file is located here.

The Imported Model

For this example, we shall assume that the result should be a single non-manifold body. do_something imports the IGES model, stitches the sheet bodies together, simplifies the spline geometry, and tightens the gaps along the tolerant edges. As in previous examples, the IGES import is performed by import_from_file. The stitching is performed by my_stitch. For this example, we stitch in a failsafe mode. Although there are no coincident faces in this model, we use the SPASTITCH_COIN_SKIP mode for processing coincident faces. If this were an actual application and there were coincident faces in the model, we would have needed to expand my_process_coincident_faces. As in the previous example, it simply lists the coincident faces in each cluster. my_stitch performs both manifold and non-manifold stitching, using api_stitch and api_stitch_nonmanifold. api_stitch is used to create the closed solid region. api_stitch_nonmanifold is used to stitch the sheet face to the solid region. Simplification of spline geometry is performed by my_simplify_entities and tightening of gaps is performed by my_tighten_gaps. Reporting of problems encountered during the healing process is identical to what we demonstrated in the Failsafe Behavior example. Each of the healing workflow actions requires a tolerance value. For this example, we simply hard coded tolerance values. In a real application, these values would depend up the factors described in Recommended Healing Workflow.

Our attempt to "make things interesting" was more successful than we anticipated. Our very loosely fit B-spline curves and surfaces exercised tolerant stitching, as we had hoped. As the output shows we are not able to simplify the two B-spline surfaces and two of the B-spline curves. Simplification did convert the two surfaces of revolution back into conical surfaces. Gap tightening did tighten some of the gaps, but it was not able to tighten the largest gaps along the non-manifold edges. The output file for this example shows the failsafe behavior of simplification and gap tightening. It does not clearly identify the types of the curves and surfaces before and after simplification, nor does it clearly show this magnitudes of the gaps before and after gap tightening. As they say, this is left as an exercise for the student.

Recommended Healing Workflow Example Output File
The imported model:
ENTITY[0]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
1 loop record,     56 bytes
1 surface record,    184 bytes
5 coedge records,    260 bytes
5 edge records,    400 bytes
5 pcurve records,    520 bytes
5 vertex records,    160 bytes
5 curve records,    904 bytes
5 point records,    320 bytes
Total storage 2988 bytes
Box: from (-2.0, -0.0, -2.0) to (2.0, 2.0, 0.0)

ENTITY[1]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
1 loop record,     56 bytes
1 surface record,    176 bytes
2 coedge records,    104 bytes
2 edge records,    160 bytes
2 pcurve records,    208 bytes
2 vertex records,     64 bytes
2 curve records,    432 bytes
2 point records,    128 bytes
Total storage 1512 bytes
Box: from (-1.5, -1.5, -2.0) to (1.5, 1.5, -2.0)

ENTITY[2]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
1 loop record,     56 bytes
1 surface record,    184 bytes
5 coedge records,    260 bytes
5 edge records,    400 bytes
5 pcurve records,    520 bytes
5 vertex records,    160 bytes
5 curve records,    904 bytes
5 point records,    320 bytes
Total storage 2988 bytes
Box: from (-2.0, -2.0, -2.0) to (2.0, 0.0, 0.0)

ENTITY[3]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
1 loop record,     56 bytes
1 surface record,    184 bytes
3 coedge records,    156 bytes
3 edge records,    240 bytes
5 pcurve records,    520 bytes
2 tcoedge records,    176 bytes
2 vertex records,     64 bytes
7 curve records,   1032 bytes
3 tvertex records,    144 bytes
7 attrib records,    308 bytes
2 tedge records,    192 bytes
5 point records,    320 bytes
7 annotation records,    336 bytes
Total storage 3912 bytes
Box: from (-0.0, -2.0, -0.0) to (2.0, 2.0, 2.0)

ENTITY[4]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
1 loop record,     56 bytes
1 surface record,    184 bytes
3 coedge records,    156 bytes
3 edge records,    240 bytes
5 pcurve records,    520 bytes
2 tcoedge records,    176 bytes
4 tvertex records,    192 bytes
1 vertex record,     32 bytes
7 curve records,   1032 bytes
8 attrib records,    352 bytes
2 tedge records,    192 bytes
5 point records,    320 bytes
8 annotation records,    384 bytes
Total storage 4020 bytes
Box: from (-2.0, -2.0, -0.0) to (0.0, 2.0, 2.0)

ENTITY[5]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
1 loop record,     56 bytes
1 surface record,    176 bytes
2 coedge records,    104 bytes
2 edge records,    160 bytes
2 pcurve records,    208 bytes
2 tvertex records,     96 bytes
2 curve records,    240 bytes
2 attrib records,     88 bytes
2 point records,    128 bytes
2 annotation records,     96 bytes
Total storage 1536 bytes
Box: from (-1.5, -1.5, 2.0) to (1.5, 1.5, 2.0)

ENTITY[6]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
1 face record,     56 bytes
2 loop records,    112 bytes
1 surface record,    176 bytes
4 coedge records,    208 bytes
4 edge records,    320 bytes
4 pcurve records,    416 bytes
4 vertex records,    128 bytes
4 curve records,    864 bytes
4 point records,    256 bytes
Total storage 2664 bytes
Box: from (-3.0, -3.0, 0.0) to (3.0, 3.0, 0.0)

There were 0 clusters of coincident faces

The stitched model:
ENTITY[0]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
6 face records,    336 bytes
6 loop records,    336 bytes
6 surface records,   1088 bytes
8 coedge records,    416 bytes
16 tcoedge records,   1408 bytes
4 edge records,    320 bytes
24 pcurve records,   2496 bytes
30 attrib records,   1320 bytes
8 tedge records,    768 bytes
28 curve records,   3952 bytes
2 vertex records,     64 bytes
6 tvertex records,    288 bytes
30 annotation records,   1440 bytes
8 point records,    512 bytes
Total storage 14872 bytes
Box: from (-2.0, -2.0, -2.0) to (2.0, 2.0, 2.0)

The stitched nonmanifold model:
ENTITY[0]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
7 face records,    392 bytes
8 loop records,    448 bytes
7 surface records,   1264 bytes
10 coedge records,    520 bytes
20 tcoedge records,   1760 bytes
6 edge records,    480 bytes
30 pcurve records,   3120 bytes
34 attrib records,   1496 bytes
8 tedge records,    768 bytes
34 curve records,   4864 bytes
4 vertex records,    128 bytes
6 tvertex records,    288 bytes
34 annotation records,   1632 bytes
10 point records,    640 bytes
Total storage 17928 bytes
Box: from (-3.0, -3.0, -2.0) to (3.0, 3.0, 2.0)

Problem encountered: did not simplify edge
Severity: PROBLEM
Problem with tedge
tedge **** 14237384:
Rollback pointer: 14188024
Attribute list  : attrib_annotation 0 591088
Coedge pointer  : tcoedge **** 14249984
Start vertex    : tvertex 0 14451696
Start parameter : 0
End vertex      : tvertex 1 14423672
End parameter   : 4.698942919923
Curve geometry  : intcurve 0 618808
Sense           : forward
Bounding box    : -0.00033422335344142 : 1.5172359121756
: -1.5003342233534 : 1.5003342233534
: 1.9996607766466 : 2.0003342233534
Convexity       : unknown
Tolerance       : 5.0000000006989e-006
Update          : Updated tolerance
Problem encountered: did not simplify edge
Severity: PROBLEM
Problem with tedge
tedge **** 14209280:
Rollback pointer: 14409512
Attribute list  : attrib_annotation 1 14492752
Coedge pointer  : tcoedge **** 14206648
Start vertex    : tvertex 1 14423672
Start parameter : -4.698942919923
End vertex      : tvertex 0 14451696
End parameter   : 0
Curve geometry  : intcurve 1 14279832
Sense           : forward
Bounding box    : -1.5172389121756 : 0.00033422335343834
: -1.5003342233534 : 1.5003342233534
: 1.9996577766466 : 2.0003342233534
Convexity       : unknown
Tolerance       : 8.0000000006741e-006
Update          : Updated tolerance
The simplified model:
ENTITY[0]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
7 face records,    392 bytes
8 loop records,    448 bytes
7 surface records,   1504 bytes
10 coedge records,    520 bytes
20 tcoedge records,   1760 bytes
6 edge records,    480 bytes
24 pcurve records,   2496 bytes
34 attrib records,   1496 bytes
8 tedge records,    768 bytes
34 curve records,   4880 bytes
4 vertex records,    128 bytes
6 tvertex records,    288 bytes
34 annotation records,   1632 bytes
10 point records,    640 bytes
Total storage 17560 bytes
Box: from (-3.0, -3.0, -2.0) to (3.0, 3.0, 2.0)

Problem encountered: Could not tighten gap within desired_gap_tightness
Severity: PROBLEM
Problem with tedge
tedge **** 14125992:
Rollback pointer: 14126960
Attribute list  : attrib_annotation 0 908176
Coedge pointer  : tcoedge **** 14483952
Start vertex    : tvertex 0 14470248
Start parameter : 1.5707963267949
End vertex      : tvertex 1 860592
End parameter   : 3.1415926535898
Curve geometry  : ellipse 0 14476128
Sense           : forward
Bounding box    : NULL
Convexity       : unknown
Tolerance       : 0.011312536591095
Update          : Updated tolerance
Problem with tedge
tedge **** 14446360:
Rollback pointer: 686432
Attribute list  : attrib_annotation 1 543768
Coedge pointer  : tcoedge **** 906680
Start vertex    : tvertex 1 860592
Start parameter : -3.1415926535898
End vertex      : tvertex 2 14491416
End parameter   : -1.5707963267949
Curve geometry  : ellipse 1 478472
Sense           : forward
Bounding box    : NULL
Convexity       : unknown
Tolerance       : 0.011313087153831
Update          : Updated tolerance
Problem with tedge
tedge **** 913200:
Rollback pointer: 536856
Attribute list  : attrib_annotation 2 14142112
Coedge pointer  : tcoedge **** 537160
Start vertex    : tvertex 2 14491416
Start parameter : 1.5707963267949
End vertex      : tvertex 3 14249544
End parameter   : 3.1415926535898
Curve geometry  : ellipse 2 599264
Sense           : forward
Bounding box    : NULL
Convexity       : unknown
Tolerance       : 0.01131308715383
Update          : Updated tolerance
Problem with tedge
tedge **** 14451552:
Rollback pointer: 14234296
Attribute list  : attrib_annotation 3 14477256
Coedge pointer  : tcoedge **** 618424
Start vertex    : tvertex 3 14249544
Start parameter : -3.1415926535898
End vertex      : tvertex 0 14470248
End parameter   : -1.5707963267949
Curve geometry  : ellipse 3 14416664
Sense           : forward
Bounding box    : NULL
Convexity       : unknown
Tolerance       : 0.01131253642071
Update          : Updated tolerance
The gap tightened model:
ENTITY[0]:
1 body record,     40 bytes
1 lump record,     40 bytes
1 shell record,     48 bytes
7 face records,    392 bytes
8 loop records,    448 bytes
7 surface records,   1504 bytes
10 coedge records,    520 bytes
20 tcoedge records,   1760 bytes
6 edge records,    480 bytes
22 pcurve records,   2288 bytes
34 attrib records,   1496 bytes
8 tedge records,    768 bytes
34 curve records,   4880 bytes
4 vertex records,    128 bytes
6 tvertex records,    288 bytes
34 annotation records,   1632 bytes
10 point records,    640 bytes
Total storage 17352 bytes
Box: from (-3.0, -3.0, -2.0) to (3.0, 3.0, 2.0)