Tutorial:ACIS Tutorial Printout

From DocR21

Jump to: navigation, search


Contents

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
  • start with api_ to make them easily identifiable.

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.
  5. Execute the program by Debug-Start Without Debugging.
  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.
  2. We need to add ACIS header files to 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

Main article: 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

Main article: Body


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

Main article: Lump


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

Main article: Shell


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

Main article: Subshell


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

Main article: Wire


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

Main article: Face


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 or 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

Main article: Loop


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

Main article: Coedge


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

Main article: Edge


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

Main article: Vertex


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

api_sweep_with_options

api_skin_wires

api_loft_coedges

api_net_wires

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.

Direct Interface Functions API Functions
get_lumps(ENTITY*, ENTITY_LIST&, ...) api_get_lumps(ENTITY*, ENTITY_LIST&, ...)
get_wires(ENTITY*, ENTITY_LIST&, ...) api_get_wires(ENTITY*, ENTITY_LIST&, ...)
get_shells(ENTITY*, ENTITY_LIST&, ...) api_get_shells(ENTITY*, ENTITY_LIST&, ...)
get_faces(ENTITY*, ENTITY_LIST&, ...) api_get_faces(ENTITY*, ENTITY_LIST&, ...)
get_loops(ENTITY*, ENTITY_LIST&, ...) api_get_loops(ENTITY*, ENTITY_LIST&, ...)
get_edges(ENTITY*, ENTITY_LIST&, ...) api_get_edges(ENTITY*, ENTITY_LIST&, ...)
get_coedges(ENTITY*, ENTITY_LIST&, ...) api_get_coedges(ENTITY*, ENTITY_LIST&, ...)
get_vertices(ENTITY*, ENTITY_LIST&, ...) api_get_vertices(ENTITY*, ENTITY_LIST&, ...)
get_tedges(ENTITY*, ENTITY_LIST&, ...) api_get_tedges(ENTITY*, ENTITY_LIST&, ...)
get_tcoedges(ENTITY*, ENTITY_LIST&, ...) api_get_tcoedges(ENTITY*, ENTITY_LIST&, ...)
get_tvertices(ENTITY*, ENTITY_LIST&, ...) api_get_tvertices(ENTITY*, ENTITY_LIST&, ...)

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. Still other applications can represent a face on a periodic spline surface without a seam edge. 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 analytic 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-percent 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 when 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:

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 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."


The API_BEGIN / API_END Family of Macros

Previously we have mentioned the API_BEGIN / API_END macros can be used to roll back the model in the event of an exception. Now we are ready to describe how this is done. In Tutorials (History Streams), we shall describe what happens to the ACIS data structure during roll back and roll forward.

The API_BEGIN / API_END macro family has four pairs of macros. These are all defined in api.hxx.

Macros Description
API_BEGIN / API_END Provide rollback behavior in the event of an exception
API_NOP_BEGIN / API_NOP_END Similar to API_BEGIN / API_END

but always rolls back the (potentially nested) block

API_TRIAL_BEGIN / API_TRIAL_END Similar to API_NOP_BEGIN / API_NOP_END

but rolls back the nested block only in the event of of an exception

API_SYS_BEGIN / API_SYS_END Used by the ACIS system functions that manipulate

the history-related data structures

Important: These macros must always be used in matched pairs, creating a block of code.


Important: These blocks should never contain a return, goto, break, or other statement that would prematurely transfer control out of the block.


Important: We often use the shorthand "API_BEGIN/END" to signify "API_BEGIN/API_END" and similarly with "API_NOP_BEGIN/END" and "API_TRIAL_BEGIN/END."


The first three sets of macros are described in the subsections below. The behavior of the system-related macros is beyond the scope of this tutorial. Before we get into the description of these macros, we need to define a couple of terms.

ACIS records sets of ENTITY changes on BULLETIN_BOARDS. A BULLETIN_BOARD is the lowest level construct that can be rolled backward or forward. When a BULLETIN_BOARD is rolled backward, all the ENTITY changes recorded on it are undone. When a previously rolled back BULLETIN_BOARD is rolled forward, the ENTITY changes are once again part of the model.

During an operation, ACIS may create stacked BULLETIN_BOARDS. That is, BULLETIN_BOARDS within BULLETIN_BOARDS. Sometimes these are referred to as nested BULLETIN_BOARDS. When the macro denoting the end of a stacked BULLETIN_BOARD is reached, the ENTITY changes recorded on the stacked (inner) BULLETIN_BOARD will either be merged with the next higher level BULLETIN_BOARD or immediately rolled back. Stacked BULLETIN_BOARDS do not remain in the persistent model.

API_BEGIN / API_END

The API_BEGIN macro does several things for you. If it is the outermost API_BEGIN it will create and open a BULLETIN_BOARD for you. Each API_BEGIN starts an EXCEPTION block for you. (API_BEGIN includes an EXCEPTION_BEGIN and EXCEPTION_TRY.) And all API_BEGINs declare an outcome called result for you.

Because all ENTITY changes must be on a BULLETIN_BOARD, all ENTITY changes must be within an API_BEGIN/END block. A common ACIS programming error is to call a direct interface function that changes an ENTITY outside of an API_BEGIN/END block.

Because API_BEGIN declares result for you, you should not redeclare result inside the block. Redeclaring result inside the API_BEGIN/END block will hide the one declared in the API_BEGIN macro. (result has been declared before the beginning of the EXCEPTION handling block so it will be in scope after the API_END.)

Each API_END macro closes its EXCEPTION handling block, but it does not resignal any exception. (API_END includes an EXCEPTION_CATCH and an EXCEPTION_END_NO_RESIGNAL.) The outermost API_END records the value of result in the BULLETIN_BOARD, indicating success or failure of the API_BEGIN/END block.

The processing of the BULLETIN_BOARD depends on the option bb_immediate_close. If bb_immediate_close has a value of TRUE, then the BULLETIN_BOARD is processed when the API_END is reached. If bb_immediate_close has a value of FALSE, then BULLETIN_BOARD is not processed until the next API_BEGIN statement. Processing a BULLETIN_BOARD implies rolling back the changes if the operation failed, or closing the BULLETIN_BOARD and possibly merging it with other BULLETIN_BOARDs if the operation succeeded. The default behavior of ACIS is to delay the processing of BULLETIN_BOARDs until the next API_BEGIN is encountered. This provides a measure of safety if you should accidentally modify an ENTITY before the next API_BEGIN.

There are several functions and options that affect the behavior of an API_BEGIN/END block. Some of these are described below.

  • api_logging(logical on_off) This API function sets the option logging to the specified value.
  • set_logging(logical on_off) This global function specifies whether BULLETINs are created in an application. This is different from api_logging, which keeps BULLETINs and BULLETIN_BOARDs hidden from the application level, but still creates them to support error recovery. You should not call this function.
  • bb_immediate_close This option controls whether bulletin boards are closed at the API_END statement or the next API_BEGIN statement.
  • compress_bb If this option is TRUE, at the end of each successful block the bulletins in the bulletin board created for that block are merged with those from the previous bulletin board, so they appear as though the operations occurred in the same block.
  • logging If this option is FALSE, each bulletin board is deleted as soon as the next is opened and application functions to get bulletin boards or delta states always return NULL. At the end of a failed API, the bulletin board is rolled and and deleted so the model is restored to its previous non-corrupted state. If the API is successful and API logging is FALSE, the bulletin board is simply deleted. This reduces memory usage, but the application will not be able to roll to previous states because no history exists.

C++ Example

In this example we have created a function that generates a BODY that consists of the intersection of a cone and a prism.

logical do_something(BODY *& my_body)
{
  logical success = TRUE;
 
  API_BEGIN
    BODY *prism = NULL;
    result = api_make_prism(..., prism);
 
    BODY *cone = NULL;
    if(result.ok())
      result = api_make_frustum(..., cone);
 
    // prism is the tool. cone is the blank. The result of
    // the api_intersect( ) call is the blank (cone). The tool 
    // (prism) is deleted.
 
    if(result.ok())
       result = api_intersect(prism, cone);
 
    // If a NULL body is returned, then there was no overlap.
    // We consider this a failure. Force a rollback
    // by setting result to a failing (non-zero) value.
 
    if (cone == NULL)
      result = outcome(API_FAILED);
  API_END
 
  // Setup our return values.
  success = result.ok();
  my_body = cone;
 
  return success;
}

There are some significant points to note about this simple example:

  • The variable result is used for determining the success or failure of the entire API_BEGIN/END block. It is even set to API_FAILED if the prism and cone do not intersect. By doing this, the API_END will examine result and, if it is non-zero, it will roll back all changes (assuming this is the outermost API_END and bb_immediate_close has a value of TRUE.)
  • In the event of any type of failure, no cleanup is needed. The rollback will perform all cleanup.
  • There are no return or goto statements inside the API_BEGIN/END block. This is important because once an API_BEGIN/END block is entered, it must be exited through its corresponding API_END.

Let us take this example one step further by considering how this routine might be called:

void main(void)
{
  API_BEGIN
 
    BODY * my_body;
    logical success = do_something(my_body);
    result = (success == TRUE) : outcome(0) ? outcome(API_FAILED);
 
    // If the result is not ok, no cleanup is  needed; all
    // results will be rolled back when we pass through API_END.
 
    if (result.ok())
      api_del_entity(my_body);
 
  API_END
}

This illustrates that nesting of API_BEGIN/END blocks is perfectly acceptable. However, there are important behavioral differences:

  • If any failures occur within do_something, its API_END will NOT rollback the changes, because the API_BEGIN/END block in do_something is "nested" within the API_BEGIN/END block in main. Any rollback will occur at the outermost API_END, which is now the API_END in main. Is it obvious that in this program a rollback will not occur if bb_immediate_close is FALSE?
  • It is important for do_something to propagate the knowledge of success or failure using its logical return value. It allows main to determine success or failure and set its result appropriately. The knowledge of success or failure must be propagated upward through API_BEGIN/END blocks to ensure that the rollback occurs at the outermost API_END.

This example does not demonstrate how to print error messages. You may refer to previous tutorials, such as Math Classes, for an illustration of this.

API_NOP_BEGIN / API_NOP_END

The API_NOP_BEGIN and API_NOP_END macros provide a means of performing a model changing operation and then immediately throwing away the changes. API_NOP is pronounced "API - NO - OP" meaning the operation performed by the API is a NULL operation.

API_NOP_BEGIN opens a stacked BULLETIN_BOARD if another BULLETIN_BOARD is already open; otherwise, it simply opens a BULLETIN_BOARD. The primary difference between a API_NOP_BEGIN/END block and a API_BEGIN/END block is that when execution reaches the API_NOP_END the (possibly stacked) BULLETIN_BOARD is immediately rolled back, regardless of the value of result. The model is returned to a state as if the ENTITY changes recorded on the (possibly stacked) BULLETIN_BOARD never occurred. These macros provide a means to query or evaluate the model without changing it. (For instance, if you wanted to create a function that would determine if two BODYs intersected, you could perform an intersection operation, determine if the result of the intersection operation was non-NULL, and then roll back the intersection operation. There are more efficient ways to perform this query, however.)

C++ Example

This example demonstrates how an API_NOP_BEGIN/END block can be used to roll back unwanted model changes when you are determining if a given position is on a given edge. In this case, the algorithm creates temporary APOINT and point_entity_rel objects and we do not want them to exist at the end of the function. The APOINT object is derived from ENTITY and will be rolled back at the API_NOP_END statement. The point_entity_rel object is not derived from ENTITY; therefore, we must delete it manually. It is destructed after the API_NOP_BEGIN/END block so that it will always be destructed, even if an exception would occur within the API_NOP_BEGIN/END block. (In this case, because the point_entity_rel is created by an API function and API functions should not throw exceptions, we do not really need to put the destruction of the point_entity_rel after the API_NOP_BEGIN/END block, but we do so to demonstrate how the block can be used for exception handling.)

logical is_on_the_edge(SPAposition const & my_loc, EDGE * my_edge)
{
  logical on_edge = FALSE;
 
  point_entity_rel * my_rel = NULL;
 
  API_NOP_BEGIN
 
    // Create a temporary APOINT based upon the given location.
 
    APOINT * my_point = ACIS_NEW APOINT(my_loc);
 
    // Determine if the location is on the interior of the edge
    // or on either of its vertices.
 
    result = api_ptent_rel(my_point, my_edge, my_rel);
    if (result.ok() && my_rel != NULL && 
        (my_rel.rel_type == point_in_entity || my_rel.rel_type == point_on_entity))
      on_edge = TRUE;
 
  API_NOP_END
 
  if (!result.ok())
    on_edge = FALSE;
 
  if (my_rel != NULL)
    ACIS_DELETE my_rel;
 
  return on_edge;
}

In this example is_on_the_edge treats the input EDGE as a const. Although this may be the desired behavior of the function, it is not the most efficient behavior. Because api_ptent_rel will use the bounding box of the EDGE and will calculate one if one does not exist, and because API_NOP_END will remove the bounding box from the EDGE if it did not exist before the API_NOP_BEGIN, this function may calculate and throw away the EDGE's bounding box. If this function were to be called many times on the same EDGE, this would be very inefficient. To be more efficient, you should call get_edge_box before calling is_on_the_edge, or add a call to get_edge_box before the API_NOP_BEGIN statement.

API_TRIAL_BEGIN / API_TRIAL_END

Like an API_NOP_BEGIN, an API_TRIAL_BEGIN opens a stacked BULLETIN_BOARD if another BULLETIN_BOARD is already open; otherwise, it simply opens a BULLETIN_BOARD. Logically it does not make sense to have an outermost API_TRIAL_BEGIN/API_TRIAL_END block, but it occasionally does happen.

The primary difference between a API_TRIAL_BEGIN/END block and a API_NOP_BEGIN/END block is that when execution reaches the API_TRIAL_END the (possibly stacked) BULLETIN_BOARD is either immediately rolled back or merged with the next higher level BULLETIN_BOARD, depending upon the value of result. Thus, these macros provide a means to try multiple algorithms within a single API function. If the first algorithm does not succeed, simply roll back the changes and try another approach.

C++ Example

In this example, we demonstrate how API_TRIAL_BEGIN/END blocks may be used to perform an operation using three alternative algorithms. If the algorithm in the API_TRIAL_BEGIN/END block fails, any model changes will immediately be rolled back at the API_TRIAL_END statement. If the algorithm is successful, the model changes on the nested BULLETIN_BOARD will be immediately merged with next higher level BULLETIN_BOARD.

logical do_something(BODY *& my_body)
{
  logical success = try_algorithm1(my_body);
 
  if (!success)
    success = try_algorithm2(my_body);
 
  if (!success)
    success = try_algorithm3(my_body);
 
  return success;
}
 
logical algorithm1(BODY *& my_body)
{
  volatile logical success = TRUE;
 
  API_TRIAL_BEGIN
 
    // Implement algorithm 1
 
  API_TRIAL_END
  if (!result.ok())
    success = FALSE;
 
  return success;
}
 
logical algorithm2(BODY *& my_body)
{
  volatile logical success = TRUE;
 
  API_TRIAL_BEGIN
 
    // Implement algorithm 2
 
  API_TRIAL_END
  if (!result.ok())
    success = FALSE;
 
  return success;
}
 
logical algorithm3(BODY *& my_body)
{
  volatile logical success = TRUE;
 
  API_TRIAL_BEGIN
 
    // Implement algorithm 3
 
  API_TRIAL_END
  if (!result.ok())
    success = FALSE;
 
  return success;
}

Some Programming Suggestions

  • Whenever you have a function that allocates heap memory or modifies the data base, think about exception handling. What happens if something goes wrong in the middle of the function? Programming always should be done with exception handling in mind.
  • Some direct interface functions that you would never suspect in fact do change ENTITYs, for instance, get_face_box. This means that a BULLETIN_BOARD must be open when you call get_face_box. This means that get_face_box must always be called from within an API_BEGIN/END block. In general, it is safest to put all direct interface calls inside an API_BEGIN/END block; at least until you learn which direct interface functions modify the model.
  • To be sure that result has been been assigned some value: it does not hurt to assign a value to it in the first line of each API_BEGIN/END block. This may prevent you from redeclaring result. For example,
result = outcome(0);
  • API_BEGIN/END blocks have more overhead associated with them than EXCEPTION blocks. Use them sparingly. Stacked BULLETIN_BOARDS (that is, use of API_NOP_BEGIN/END or API_TRIAL_BEGIN/END inside of API_BEGIN/END blocks) have even more overhead associated with them. Use them even more sparingly.
  • Rolling back a BULLETIN_BOARD returns an ENTITY to the state it was in before the BULLETIN_BOARD was opened. This includes portions of ENTITYs like bounding boxes. To prevent repeatedly calculating bounding boxes inside API_NOP_BEGIN/END or failing API_TRIAL_BEGIN/END blocks, try to calculate bounding boxes before entering the blocks.
  • To make ACIS programming simpler, you can define macros to process the results of API_BEGIN/END blocks. For example, to check the value of result and print an error message:
#define PROCESS_RESULT                 \
  if (!result.ok())                    \
    print_error_mess(result.error_number( ), stderr);
In other tutorials we use the following macro to check the value of result, print an error message, and resignal 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);                               \
  }
In other tutorials we use the following macro to check the value of result, 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 erro_no;                                  \
  }
If you are not interested in printing an error message, just resignalling an error, we suggest that you use void check_outcome(const outcome&). This is defined in ckoutcome.hxx.

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