Showing posts with label WebLink. Show all posts
Showing posts with label WebLink. Show all posts

Thursday, October 1, 2009

Pro/WebLink: Replace Drawing Formats

Jürgen from Germany was asking me recently about replacing formats with Pro/WebLink. In the process of providing a solution, I found an interesting combination of disappointments, curiosities, and finally a very nice solution.

SetSheetFormat()

Looking through the API documentation, it's not too hard to find that the method call to replace drawing formats is SetSheetFormat() in the pfcSheetOwner class. One of the primary arguments is the drawing format object in the form of a pfcDrawingFormat object. Unfortunately, this object's class is not obviously documented. The docs say only that its parent class is a pfcModel. So how to you get a drawing format object when it seems to have no methods?

Since it's a subclass of pfcModel, you use pfcBaseSession methods such as OpenFile(), GetModel, and RetrieveModel(). Here's where the J-Link docs are bit better, because they list the methods inherited from parent classes and interfaces, just making it that much more obvious. To use RetrieveModel(), a pfcModelDescriptor object needs to be created which specifies what is to be retrieved and how.

Here is the core sequence of steps, assuming the current model is a drawing:

var session = pfcGetProESession();
var drawing = session.CurrentModel;
var drwSheetNum = drawing.CurrentSheetNumber; // use current sheet

var frmName = "c_size.frm";
var frmSheetNum = 1;
var frmDescr = pfcCreate("pfcModelDescriptor").CreateFromFileName(frmName);
var format = session.RetrieveModel(frmDescr);

var drwModel = null;
drawing.SetSheetFormat(drwSheetNum, format, frmSheetNum, drwModel);
 

Format Folder

You'll notice that the folder where the format is stored was not specified. Why? According to the docs, you can't specify the path to a folder containing a pfcModel when using RetrieveModel(). Although the pfcModelDescriptor object contains a Path property, it is ignored.

How does Pro/Engineer know where to find the format? It just does. Well, if the PRO_FORMAT_DIR config.pro option is set properly, then it just does. Fortunately, this config.pro option can handle folder paths, Intralink 3.x folders, and PDMLink folders. That should cover most scenarios.


Format Sheets

SetSheetFormat() also allows you to specify the sheet number of the format to use (should your format have more than one sheet). Unfortunately, since the pfcDrawing Format object does not implement the pfcSheetOwner interface, the developer has no way to determine how many sheets are in the format. You'll just have to know how many sheet your formats have ahead of time, not a major tragedy.

To be fair this is a Pro/Toolkit limitation first and foremost. The PFC APIs merely inherit Pro/Toolkit's limitations.

Drawings, layouts, and Pro/Report files (who uses these anyway?!) all are defined as pfcModel2D objects, which gives lots of info about sheets, tables, and detail items. Formats, although pretty much the same thing, are not pfcModel2D objects. As a result, we can get none of these things.


Drawing Model

The drawing model is the part or assembly documented on the current sheet. Although this can be specified as the last argument to SetSheetFormat(), we don't have the ability to know what the current model is (at least until WF4). null is used to here to indicate the current drawing model.


Format Tables

Just as with interactive use of Pro/Engineer, when drawing formats are replaced, the drawing tables from the previous format have to be dealt with. Typically, this involves deleting the tables. Since SetSheetFormat() does not provide a mechanism or option for removing the old format tables, this will have to be done with more code.

This is where it gets a little tricky. While there is a CheckIfIsFromFormat() method call that will tell us if a table came from a format, we'll need the table object first. To get the table object, first we'll have to use ListTables() from the pfcTableOwner class to get all of the tables for the entire drawing. ListTables() takes no argument, meaning that we have no way to pre-filter the collection of tables. It really should take a sheet number argument to return only the tables on that sheet. As an alternatives, had the pfcTable class been a pfcDetailItem (in addition to a pfcModelItem), we could have gathered the tables by sheet number using ListDetailItems(), from pfcDetailItemOwner.

var tables = drawing.ListTables();
 
The workflow for CheckIfIsFromFormat() is a bit odd as well. To check if a table is from a format, we need the pfcTableObject, and then we need to specify the sheet number. Now wait a minute, the function returns a boolean so logically the table is from the format or it is not. What does the sheet number have to do with that? Even if the table had been segmented, it's still going to be from the format or not.

if (table.CheckIfIsFromFormat(drwSheetNum) == true) {
// table is from format, delete it
drawing.DeleteTable(table, false);
}
 
This quirkiness turns out to be very useful because it's rather difficult to tell on what sheet the table exists. The pfcTableInfo class does not contain this info. While it's true that the GetSegmentSheet() method can be used for this, we would have to assume that segment zero is the proper segment to use.


Full Example: Replacing Formats

This example shows a complete code set for obtaining the drawing object, the format object, and their related info. It then procedes to call the ReplaceFormat() function, which removes the old format tables prior to replacing the format. The code replaces the format and tables very quickly.

// get current drawing and sheet number
var session = pfcGetProESession();
var drawing = session.CurrentModel;
var drwSheetNum = drawing.CurrentSheetNumber;

// get format name from html text field
var frmName = document.getElementById("frmName").value

// use format sheet 2 if drawing sheet is not sheet 1
var frmSheetNum = 1;
if (drwSheetNum > 1) { frmSheetNum = 2; }

// get the format
var frmDescr = pfcCreate("pfcModelDescriptor").CreateFromFileName(frmName);
var format = session.RetrieveModel(frmDescr);

// replace the format
ReplaceFormat(drawing, drwSheetNum, format, frmSheetNum);

// update the display (optional)
// session.CurrentWindow.Repaint();
// drawing.Regenerate();


function ReplaceFormat ( drawing, drwSheetNum, format, frmSheetNum ) {

var tables = null;

try {
// get sequence of tables in the drawing
tables = drawing.ListTables();
for (var i=0; i<tables.Count; i++) {
var table = tables.Item(i);
if (table.CheckIfIsFromFormat(drwSheetNum) == true) {
// table is from format, delete it
drawing.DeleteTable(table, false);
}
}
}
catch (e) {
// ignore, no tables in drawing
}

drawing.SetSheetFormat(drwSheetNum, format, frmSheetNum, null);

}
 
Another step to perform (marked as optional in the example) is to update the display using either Repaint() or Regenerate(). They both should accomplish the same goal, but one may work better in certain drawings than the other. You can be sure that if a large drawing is slow to repaint when performed in Pro/Engineer interactively, it will be just as slow via Pro/Web.Link.

Wednesday, July 2, 2008

Pro/WebLink: Tips On Getting Started

Pro/WebLink is one of the free PFC based API's available for automating Pro/Engineer. Although the documentation and marketing literature is focused on using Javascript with Pro/WebLink, VBScript can be used if that is more to your taste.

Developers interested in writing Pro/WebLink application have to overcome a number of hurdles. Most of these are related to the Internet Explorer and Mozilla security models, because Pro/WebLink applications typically must run within the context of the Pro/Engineer embedded web browser. This differs from the VB API (new with Wildfire 4.0), which runs outside of the Pro/Engineer process and has far fewer security issues to worry about.

While security is not the only problem facing Pro/WebLink developers, it is the most common problem in my experience. In this article I'll discuss the necessary steps in setting up your environment to run Pro/WebLink applications.

Pro/ENGINEER Configuration

Use of Pro/WebLink requires that the config.pro option WEB_ENABLE_JAVASCRIPT be set to ON (the default is OFF). In Windows, if this option is set to ON when Pro/ENGINEER is started, pfcscom.dll will be registered as a COM server. This contains all of the Pro/WebLink functionality.

Generally this works very well, but I have seen problems where the DLL doesn't get registered, or some other similar problem occurs preventing the Pro/WebLink application from functioning. The workaround is to run the application as an administrative user first, then the program works for everyone. Fortunately, I have only seen one user affected like this, otherwise the workaround would get really annoying.

None of that is relevant in Unix, as the required XPCOM technology is already present in the embedded Mozilla browser. However, I have seen rare instances where the Mozilla profile in the user's unix home directory has become corrupted. The resolution is to delete the profile. Pro/Engineer will recreate it upon startup.

HTML SCRIPT Tag

When setting up your Pro/WebLink programs, you are likely to include one or more files containing your Javascript code. A common one to use, although it is not absolutely necessary, is the PTC supplied pfcUtils.js file. If you are going to use such functions as pfcCreate() and pfcGetProESession(), you need to make sure you include pfcUtils.js correctly to avoid confusing errors, such as "Object Expected".

You also need to ensure that the SCRIPT tag entry in your HTML code correctly locates the pfcUtils.js file. For example, this entry will find the bom2excel.js file if it is placed in the same folder as the HTML file, and the pfcUtils.js file if it is in the parent folder:

<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="../pfcUtils.js"></SCRIPT>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="bom2excel.js"></SCRIPT>
 

Your web browser may not complain in an obvious way if it cannot find one of your Javascript programs, making the resulting errors hard to diagnose. If using a web server, make sure that the web server process has sufficient permissions to read the HTML and JS files, otherwise you'll wind up with the same problems, but it will that much harder to diagnose.

Web Browser Security Models

Overcoming the security requirements of Wildfire's various supported web browsers is often frustrating because the requirements are not inherently obvious and the documentation can be hard to find. With Wildfire 4.0, Mozilla is the supported browser on Unix platforms and Internet Explorer is the browser for Windows. Both have different mechanisms and rules concerning security.

Using digitally signed applications allow the developer to bypass some security roadblocks by requesting enhanced functionality based on the user's "trust" of the developer. Digital signatures are far beyond the scope of this article however.

Internet Explorer Security

Internet Explorer security is based on zones: Internet, Local intranet, Trusted sites, and Restricted sites. Each zone has it own security settings enabling or disabling certain functionalities. Using the "Custom Level" button, each option can be enabled or disabled selectively. If you change any of the security options, you may need to restart Pro/Engineer for the changes to take effect in the embedded web browser.
It is recommended that you do not reduce security for the Internet zone. It would be better to add the web site to the Trusted sites zone, then adjust the security for that zone.

Internet Explorer will decide to which zone your application applies depending on how you access it. If you access your program from a web server using a fully qualified domain name (i.e. www.yahoo.com) for a site that is not in your Trusted sites list, Internet Explorer will consider it part of the Internet zone. Using an internal web server (i.e. without top level domain: .com, .org, .co.uk, etc) or accessing the program from your hard drive or a network hard drive, will cause Internet Explorer to use the Local intranet zone. Not surprisingly, the Trusted sites zone will apply to a site that is in the Trusted sites list, even if it is an internal web server.

Pro/WebLink applications need a couple of settings defined for the zone that will be used by your application. Some of these may already be the default for you. As PTC recommends in the documentation, the option "Initialize and script ActiveX controls not marked safe for scripting" should be set to "Prompt". If you are interacting with Excel from Pro/WebLink, this option may need to be set to "Enable".


Ensure that the option "Active Scripting" is set to "Enable".

Mozilla Security

Mozilla has some good security functionality in which a script can request enhanced functionality allowing the user to accept or deny the request. That's why you see some UniversalXPConnect requests in Pro/WebLink examples. The code has to ask for enhanced privileges to connect to Pro/Engineer and do something useful.

When the Pro/WebLink application is served from a web server, the code and HTML must be digitally signed (together) for this request process to work. To make matters worse, a very unusual jar:http: URL must be used to interact with the resulting JAR file that contains the code and HTML.

A workaround is not to use a web server, but instead access the code and HTML from the file system using a file: type URL. For Pro/WebLink applications, the user is generally prompted only once for enhanced privileges, then is good to from then on.

Review

Keeping some of these issues in mind will help you get up and running with Pro/WebLink avoiding many of the pitfalls and frustrations.

  • Set WEB_ENABLE_JAVASCRIPT to ON in your config.pro file
  • Ensure that HTML and JS files are located correctly
  • Make sure that the web browser security requirements are satisfied

Wednesday, June 18, 2008

Pro/E VB API: Not Just for Visual Basic Anymore

In a previous article entitled "WebLink: What is it anyway?", I discussed Pro/WebLink and how it's not exactly a Javascript interface. Now it's the VB API's turn. The VB API is newly released by PTC with Wildfire 4. It is billed as a Visual Basic interface to PTC's PFC libraries, which is the same core that J-Link and Pro/WebLink are built upon.

While this is true, it's not that limited. Don't let the name scare you away from using your favorite COM accessible programming or scripting language to write Pro/Engineer applications. This is a good thing because it opens the API to a much wider range of applications.

In this article, I'll walk through sample applications written in Visual Basic, VBScript (using Windows Script Host), Perl, and Javascript (in a HyperText Application), all using the VB API. The sample application will connect to a Pro/Engineer session, obtain the session object, display the name of the current model, then disconnect from the Pro/Engineer session. Each program will display the model name in a different way.

Suggested reading is the section on the importance of disconnecting from Pro/Engineer in Pro/E VB API: A First Look.

Excel Macro

This is the standard and documented approach to the sample program. Objects are declared up front, the connection to Pro/Engineer is made, then the session object is obtained. The model name is displayed both in cell A1 and in a MsgBox.

Sub Macro1()

Dim asynconn As New pfcls.CCpfcAsyncConnection
Dim conn As pfcls.IpfcAsyncConnection
Dim session As pfcls.IpfcBaseSession
Dim mdlname

Set conn = asynconn.Connect("", "", ".", 5)
Set session = conn.session

mdlname = session.CurrentModel.Filename
Range("A1").Select
ActiveCell.FormulaR1C1 = mdlname
MsgBox ("Name: " & mdlname)
conn.Disconnect(2)

End Sub
 

VBScript

The VBScript code is almost identical except that VBScript doesn't seem to allow for the type declaration of the objects. As a result, the CreateObject() call is used to instantiate a pfcAsyncConnection object.

The program is executed using the following command line:
  cscript vbapi_script.vbs
 

Dim asynconn
Dim conn
Dim session
Dim mdlname

Set asynconn = CreateObject("pfcls.pfcAsyncConnection")
Set conn = asynconn.Connect("", "", ".", 5)
Set session = conn.session

mdlname = session.CurrentModel.Filename
MsgBox ("Name: " & mdlname)
conn.Disconnect(2)
 

Perl

As with most COM applications, the Perl syntax is also very similar to the VBScript code, but with Perl's own syntactical flavor. Win32::OLE->new() is the Perl equivalent to VBScript's CreateObject(). The Perl program outputs the model name to the command prompt (or standard output).

use Win32::OLE;
$asynconn = Win32::OLE->new("pfcls.pfcAsyncConnection");
$conn = $asynconn->Connect( "", "", ".", 5 );
$session = $conn->Session;
$mdlName = $session->CurrentModel->FileName;

print "mdlName: $mdlName", "\n";
$conn->Disconnect(2);
 

Javascript

Pro/WebLink applications can finally break out of the embedded browser jail using the VB API. This example uses a "Hypertext Application", which is a web page with a special HTA tag and with a file extension of ".hta" instead of ".htm". The pfcUtils.js file cannot be used as-is because it tries to use COM objects with "pfc." prefixes, instead of those associated with VB API which have "pfcls." prefixes.

Other than those differences, it's essentially a Pro/WebLink application. As with the other examples, because it's an asynchronous application, the code must connect to the Pro/Engineer session. This is a step that embedded browser based Pro/WebLink applications don't have to worry about.

<html>
<head>
<title>VB API Test</title>

<HTA:APPLICATION
ID="vbapi-test"
APPLICATIONNAME="VB API Test"
SCROLL="auto"
SINGLEINSTANCE="yes"
>
</head>

<body>

<SCRIPT LANGUAGE="JavaScript">

function HitMe ( ) {
var obj = null;
var elem = document.getElementById("mesg");

if (obj == null) {
try {
obj = new ActiveXObject("pfcls.pfcAsyncConnection");
}
catch (e) {
elem.innerHTML = "Failed to create object";
return;
}
}

var conn = obj.Connect( "", "", ".", 5 );
var session = conn.Session;
var mdlName = session.CurrentModel.FileName;

elem.innerHTML = "mdlName: " + mdlName;
conn.Disconnect(2);
}

</SCRIPT>

<form name="f">
<INPUT name="a" type=button value="Hit me!" onclick="HitMe()">
<br><a id="mesg"></a><br>
</form>

</body>
</html>
 

These are just a few of the possibilities. As you can see, there is nothing really Visual Basic specific about the VB API. It's just an API.

Tuesday, June 17, 2008

Pro/WebLink: Send Excel Data To Pro/ENGINEER Drawing Tables, and Back Again - Part 2

In part 1, the transfering of data from Excel to Pro/ENGINEER drawing tables was discussed. In this second part, the discussion will focus on the other direction, sending Pro/ENGINEER drawing table data to an Excel workbook.

TableToExcel Function

The TableToExcel() function is also pretty simple. After resetting the status fields, it uses ArrayFromProETable() to obtain an array of data from a user selected drawing table, then uses ArrayToExcel() to populate a new Excel workbook with the data from the array.

function TableToExcel ( ) {

var mesg = document.getElementById("mesg");
var errmesg = document.getElementById("errmesg");
mesg.innerHTML = "";
errmesg.innerHTML = "";

var array = ArrayFromProETable();
if (array == null) {
mesg.innerHTML = "null array";
return;
}

if (array != null) { ArrayToExcel(array); }
}
 

ArrayFromProETable Function

The ArrayFromProETable() function gathers drawing table data into an array. It's main steps are: Get the Pro/Engineer session object, obtaining the current model object and ensure a drawing is active, request a drawing table selection from the user, and iterate through the table cells to populate the array elements. After the table is selected by the user, UnHighlight() is used to repaint the table with its normal colors.

The table cell iteration code cycles first through the table rows, then the columns of each row, then through the lines of text in each cell. When there is more than one line of text in a table cell, a linefeed character is added as a line separator. This is correctly interpreted by Excel as multi-line data.

It's important to note that while in Javascript (as in many programming languages) arrays are indexed starting with zero, both drawing tables and Excel cells are indexed starting with one.

Once the iteration of the table cells has completed and the array has been populated, the array is returned.

function ArrayFromProETable ( ) {

var mesg = document.getElementById("mesg");
var errmesg = document.getElementById("errmesg");
var session = null;
var drawing = null;


// Get ProE session object
//
try {
session = pfcGetProESession();
}
catch (e) {
errmesg.innerHTML = "Unable to connect to Pro/Engineer";
return null;
}


// Get model object and ensure it's a drawing
//
try {
drawing = session.CurrentModel;

if (drawing == null || drawing.Type != pfcCreate("pfcModelType").MDL_DRAWING) {
errmesg.innerHTML = "A drawing must be active!";
return null;
}
}
catch (e) {
errmesg.innerHTML = "A drawing must be active.";
return null;
}


// Prompt user to select an existing table
//
var selections = null;
var table = null;

try {
selections = selectItems("dwg_table", 1);
var tabnum = selections.Item(0).SelItem;
table = drawing.GetTable(tabnum.Id);
}
catch (e) { // nothing selected
errmesg.innerHTML = "A drawing table was not selected.";
return null;
}

selections.Item(0).UnHighlight();


mesg.innerHTML = "Selected Table: "
+ table.GetRowCount() + " rows, "
+ table.GetColumnCount() + " columns";


// Gather data from table and populate into array
//

var array = new Array();

for (var i=0; i<table.GetRowCount(); i++) {

array[i] = new Array();

for (var j=0; j<table.GetColumnCount(); j++) {

// Table cell indexes start with one, arrays with zero
var cell = pfcCreate("pfcTableCell").Create(i+1,j+1);
var mode = pfcCreate("pfcParamMode").DWGTABLE_NORMAL;
array[i][j] = "";

try {
var textseq = table.GetText(cell, mode);

for (var k=0; k<textseq.Count; k++) {
var textitem = textseq.Item(k);
if (k > 0) { array[i][j] += "\n"; }
array[i][j] += textitem;
}
}
catch (e) { // cell has no value
// ignore
}

}

}

return array;
}
 

ArrayToExcel Function

The ArrayToExcel() function takes the two dimensional array and populates cells in a new Excel workbook. There are only three steps in this function: Get the Excel session object, Create a new workbook and get the active sheet object, Populate cells of the active sheet from data in the array.

As an alternative, a new Excel session could be started if an existing one can not be found.

function ArrayToExcel ( array ) {

var oXL;
var errmesg = document.getElementById("errmesg");


// Try to access Excel and get Application object.
//
try {
oXL = GetObject("","Excel.Application");
if (oXL == null) {
errmesg.innerHTML = "Failed to get Excel session object.";
return null;
}
}
catch (e) {
// oXL = new ActiveXObject("Excel.Application");
errmesg.innerHTML = "Excel must be running!";
return;
}


// Make session visible, get Sheet object
//
try {
oXL.Visible = true;
var oWB = oXL.Workbooks.Add();
var oSheet = oWB.ActiveSheet;
}
catch (e) {
errmesg.innerHTML = "Problem creating new workbook.";
return;
}


// Put the array data into the cells of the active sheet
//
for (var i=0; i < array.length; i++ ) {
for (var j=0; j < array[i].length; j++ ) {
// Excel cell indexes start with one, arrays with zero
oSheet.Cells(i+1, j+1).Value = array[i][j];
}
}

}
 

selectItems Function

The selectItems() function is largely based on selectItems() from an example in the Pro/WebLink documentation. It is changed here to be slightly more generic. The function builds a pfcSelectionOptions object that defines the type and number of allowable items that can be selected. It also minimizes the browser window during the selection process.

A sequence of selections (or null if nothing was selected) is returned.

function selectItems ( options, max ) {

// Setup options object
selOptions = pfcCreate("pfcSelectionOptions").Create(options);
selOptions.MaxNumSels = parseInt(max);


var session = pfcGetProESession();
var browserSize = session.CurrentWindow.GetBrowserSize();
session.CurrentWindow.SetBrowserSize(0.0);


var selections = null;

try {
selections = session.Select(selOptions, null);
session.CurrentWindow.SetBrowserSize(browserSize);
}
catch (err) {
session.CurrentWindow.SetBrowserSize(browserSize);
// In case user didn't select expected item
var errstr = pfcGetExceptionType(err);
if (errstr == "pfcXToolkitUserAbort" || errstr == "pfcXToolkitPickAbove") {
return null;
}
}

if (selections == null || selections.Count == 0)
return null;


return selections;
}
 


As I've mentioned, interaction with Excel is easy and straightforward. Although the Pro/Engineer interaction code is much more complex, the PFC API's contain a rich set a classes and methods making automation, such as I have shown here, possible. It's a quantum leap beyond relying upon mapkeys.

Monday, June 9, 2008

Pro/WebLink: Send Excel Data To Pro/ENGINEER Drawing Tables and Back Again - Part 1

In previous articles (BOM Data to Excel, Point Data to Excel), I've discussed the transfer of data from Pro/ENGINEER to Excel. Of course, transfering the data in the other direction, from Excel to Pro/Engineer, is not only possible, but it's just as easy.

In this two part article series, I will be discussing how to take data from a selected range in Excel and transfer that into a new table in a Pro/ENGINEER drawing, and vice versa. Part 1 concerns the Excel to Pro/Engineer transfer, and part 2 concerns the Pro/Engineer to Excel transfer.

HTML Page

The HTML page for this application is very simple: two javascript files, two buttons, and two status fields. pfcUtils.js is provided by PTC (see my pfcCreate optimization article).

All remaining code from this article is in the excel2table.js file. One of the buttons allows transfer from Excel to ProEngineer using the ExcelToTable() function, while the other allows transfer in the other direction using the TableToExcel() function. The two div fields allow for the display of error and general status information.

<HTML>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="pfcUtils.js"></SCRIPT>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="excel2table.js"></SCRIPT>
<BODY>

<form name="f">
<table border=1>
<tr>
<td>Excel</td>
<td>
<INPUT name="button1" type=button value=">>" onclick="ExcelToTable()">
<br>
<INPUT name="button2" type=button value="<<" onclick="TableToExcel()">
</td>
<td>Drawing Table</td>
</tr>
</table>

<br><font color='red'><a id="errmesg"></a></font>
<br><a id="mesg"></a><br>
</form>

</BODY>
</HTML>
 

ExcelToTable Function

The core of ExcelToTable() is pretty basic. Other than resetting the status fields, it uses ArrayFromExcel() to obtain an array of Excel data and ArrayToProETable() to create a drawing table from the array.

function ExcelToTable ( ) {
var mesg = document.getElementById("mesg");
var errmesg = document.getElementById("errmesg");
mesg.innerHTML = "";
errmesg.innerHTML = "";

var array = ArrayFromExcel();

if (array == null) {
mesg.innerHTML = "null array";
return;
}

if (array != null) { ArrayToProETable(array); }
}
 

ArrayFromExcel Function

The ArrayFromExcel() function has three main tasks: get the Excel session object using GetObject(), get the current range selection object, then loop through the rows and columns of the selection and extract the cell values.

Looping through the selection is done using Enumerator objects, one for rows and one for columns. The rows Enumerator is created using the 'rows' property of the selection object, then a for loop is used to iterate over the rows. For each row, a columns Enumerator is created using the 'columns' property, which references the selected columns of the row. The second for loop iterates over those columns, which in this case are cells.

The Enumerator has an item() method which is used to obtain the row object from the row Enumerator and the cell object from the column Enumerator. The 'value' property returns the cell value, which would usually be of type 'Number' or 'String'. Regardless of the type, the value is coerced into a String for ease of processing later. Pro/ENGINEER treats all table cell values as strings anyway, so it doesn't really matter too much what the type is.

The array used is two dimensional, populated in the iteration of the selection. A two dimensional array is created by first constructing an Array object, then populating it with array objects. The elements of the internal arrays contain the cell values. In the iteration, as a new row is processed, a new Array object is created and then assigned to the top level array. In Javascript, elements can be added to an Array object by a simple assignment statement, almost endlessly.

Once the iteration of the selection via Enumerator's has completed and the array has been populated, the array is returned.

function ArrayFromExcel ( ) {

var oXL = null;
var mesg = document.getElementById("mesg");
var errmesg = document.getElementById("errmesg");


// Try to access Excel and get Application object.
//
try {
oXL = GetObject("","Excel.Application");
if (oXL == null) {
errmesg.innerHTML = "Failed to get Excel session object.";
return null;
}
}
catch (e) {
errmesg.innerHTML = "Excel must be running!";
return null;
}


// Get Selection object
var sel = null;

try {
sel = oXL.ActiveWindow.Selection;
}
catch (e) {
errmesg.innerHTML = "Could not get Selection object.";
return null;
}

if (sel.columns.count <= 1 && sel.rows.count <= 1) {
errmesg.innerHTML = "A range of cells must be selected in Excel.";
return null;
}

mesg.innerHTML = "Selection: " + sel.rows.count + " rows, " + sel.columns.count + " columns";


var i_row = 0;
var e_rows = new Enumerator(sel.rows);
var array = new Array();


// Loop through each row of the selection
//
for ( ; !e_rows.atEnd(); e_rows.moveNext() ) {

var i_col = 0;
var row = e_rows.item();
var e_cols = new Enumerator(row.columns);

array[i_row] = new Array();

// Within the row, loop through each column
//
for ( ; !e_cols.atEnd(); e_cols.moveNext() ) {

var cell = e_cols.item();
var val = cell.value;

if (val == null) { val = "-"; }
array[i_row][i_col] = "" + val;
i_col++;
}

i_row++;
}

return array;
}
 

ArrayToProETable Function

The ArrayToProETable() function takes an array and creates a drawing table. Its main steps are: get the ProE session object, prompt the user for the table location, create the table creation instructions object, create the row and column specification objects, create the table, add content to the table cells, and display the table. As you can see, the ProE portion is quite a bit more complicated than the Excel part.

Included in the function, part of the row and column specs, are calculations to auto-size the rows and columns to fit the data in Excel. This is primarily for readability of the resulting table making it optional, but very helpful.

Also included is a workaround for a table creation bug that limits the size of a table to no more than 50 rows and/or no more than 50 columns. This is a bug in all Pro/Engineer API's, even Pro/Toolkit and J-Link. It doesn't mean that a huge table can't be created, just that it can't be created in one shot.

The workaround in the function is to create the table with a single row or column then add more rows or columns as necessary to get the table to the requested size. With the adjustment step (adding rows and/or columns) coming after the table creation step, but before the table display step, the time required by the workaround is hardly noticable, except with huge tables.

function ArrayToProETable ( array ) {

var errmesg = document.getElementById("errmesg");
var session = null;
var drawing = null;


// Get ProE session object
//
try {
session = pfcGetProESession();
}
catch (e) {
errmesg.innerHTML = "Unable to connect to Pro/Engineer";
return null;
}


// Get model object and ensure it's a drawing
//
try {
drawing = session.CurrentModel;

if (drawing == null || drawing.Type != pfcCreate("pfcModelType").MDL_DRAWING) {
errmesg.innerHTML = "A drawing must be active!";
return null;
}
}
catch (e) {
errmesg.innerHTML = "A drawing must be active.";
return null;
}


// Get location for new table by mouse click
//
var location = getMousePickPosition("Select location for table", true);
if (location == null) { return null; }


// Setup the table creation instructions object
//
var instrs = new pfcCreate("pfcTableCreateInstructions").Create(location);
instrs.SizeType = pfcCreate("pfcTableSizeType").TABLESIZE_BY_NUM_CHARS;


// Figure out max sizes to auto-expand the table
var ArrayMaxData = getArrayMaxData(array);


// Generate column data (for table creation instructions object)
// API Bug: Can't create table with more than 50 columns
// Workaround: Create with one column, add more later
//

var column_just = pfcCreate("pfcColumnJustification").COL_JUSTIFY_LEFT;
var columnData = pfcCreate("pfcColumnCreateOptions");
var columnOption = pfcCreate("pfcColumnCreateOption");
var maxOneShotCols = 50; // Most columns that can be created without workaround

if (ArrayMaxData.cols < maxOneShotCols) {
// Number of columns is ok, setup instructions to create table as-is.
// Use max column widths to auto-size columns
for (var i=0; i < ArrayMaxData.max_col_width.length; i++ ) {
var column = columnOption.Create(column_just, ArrayMaxData.max_col_width[i]);
columnData.Append(column);
}
}
else {
// Too many columns to create table as-is, setup instructions
// to create table with one column, expand after creation
var column = columnOption.Create(column_just, ArrayMaxData.max_col_width[0]);
columnData.Append(column);
}

// Add column data object to table creation instructions
instrs.ColumnData = columnData;


// Generate row data (for table creation instructions object)
// API Bug: Can't create table with more than 50 rows
// Workaround: Create with one row, add more later
//

var rowData = pfcCreate("realseq");
var maxOneShotRows = 50; // Most rows that can be created without workaround

if (ArrayMaxData.rows < maxOneShotRows) {
// Number of rows is ok, setup instructions to create table as-is.
// Use max row heights to auto-size rows
for (var i = 0; i < array.length; i++) {
rowData.Append(ArrayMaxData.max_row_height[i]);
}
}
else {
// Too many rows to create table as-is, setup instructions
// to create table with one row, expand after creation
rowData.Append(ArrayMaxData.max_row_height[0]);
}

// Add row data object to table creation instructions
instrs.RowHeights = rowData;


// Create the table (no contents yet, and won't be visible yet)
//
var dwgTable = null;

try {
dwgTable = drawing.CreateTable(instrs);
}
catch (e) {
errmesg.innerHTML += "Cannot create table: " + e.message;
return;
}


// Add rows if data had more than allowed to create in one shot
//
try {
if (ArrayMaxData.rows >= maxOneShotRows) {
for (var i=1; i < array.length; i++ ) {
// Add remaining rows needed. Index starts
// at 1 because one row already exists
dwgTable.InsertRow(ArrayMaxData.max_row_height[i], null, false)
}
}
}
catch (e) {
errmesg.innerHTML += "Cannot add rows: " + e.message;
return;
}


// Add columns if data had more than allowed to create in one shot
//
try {
if (ArrayMaxData.cols >= maxOneShotCols) {
for (var i=1; i < ArrayMaxData.max_col_width.length; i++ ) {
// Add remaining columns needed. Index starts
// at 1 because one column already exists
dwgTable.InsertColumn(ArrayMaxData.max_col_width[i], null, false);
}
}
}
catch (e) {
errmesg.innerHTML += "Cannot add columns: " + e.message;
return;
}


// Fill in each cell with data
//
for (var i=0; i < array.length; i++ ) {
for (var j=0; j < array[i].length; j++ ) {
// Table cell indexes start with one, arrays with zero
writeTextInCell(dwgTable, i+1, j+1, array[i][j]);
}
}


// Display the table
dwgTable.Display ();
}
 

getMousePickPosition Function

The getMousePickPosition() function is used to prompt the user for a position by using the left mouse button. It uses the UIGetNextMousePick() method of the pfcSession object to accomplish this. The function also hides the browser window, displays an alert dialog to prompt the user, and then restores the browser window to its previous size.

function getMousePickPosition ( prompt, hideBrowser ) {

var browserSize = null;
var session = pfcGetProESession();

if (hideBrowser) {
// Minimize browser, remember browser size for later restoration
browserSize = session.CurrentWindow.GetBrowserSize();
session.CurrentWindow.SetBrowserSize(0.0);
}

// Prompt user and get user specified location
alert(prompt);
var mouseButton = pfcCreate("pfcMouseButton");
var mouseClick = session.UIGetNextMousePick(mouseButton.MOUSE_BTN_LEFT);

if (hideBrowser) {
// Restore browser window
session.CurrentWindow.SetBrowserSize(browserSize);
}

return mouseClick.Position;
}
 

getArrayMaxData Function

The getArrayMaxData function analyzes an array and returns an object containing four properties. Two properties are the number of rows and columns in the array, which is expected to be two dimensional. The other two properties are maximum column widths for each column and maximum row heights for each row.

If a row in excel contains a cell with a single linefeed character, that row will have a maximum row height of at least two characters. Similarly, if a column contains a cell with 50 characters, the maximum column width for that column will be at least 50 characters.

Once these values are populated, the object is returned to the calling function.

function getArrayMaxData ( array ) {

var obj = new Object();

// Figure out max size for each row and column to auto-expand
//
obj.max_col_width = new Array();
obj.max_row_height = new Array();

for (var i=0; i < array.length; i++ ) {
obj.max_row_height[i] = 0;
}

for (var i=0; i < array[0].length; i++ ) {
obj.max_col_width[i] = 0;
}

obj.rows = array.length;
obj.cols = 0;

// populate arrays of max column widths and max row heights
//
for (var i=0; i < array.length; i++) {

for (var j=0; j < array[i].length; j++) {

if (obj.cols < array[i].length) { obj.cols = array[i].length; }

var lines = array[i][j].split("\n");

if (lines.length > obj.max_row_height[i]) {
obj.max_row_height[i] = lines.length;
}

for (var k=0; k < lines.length; k++ ) {
if (lines[k].length > obj.max_col_width[j]) {
obj.max_col_width[j] = lines[k].length;
}
}

}

}

return obj;
}
 

writeTextInCell Function

The writeTextInCell() function writes text to a specific table cell. If the cell data in Excel contains linefeeds, the result will be a table cell that matches with the same number of lines of text.

function writeTextInCell ( table, row, col, text ) {

var cell = pfcCreate("pfcTableCell").Create(row, col);
var lines = pfcCreate("stringseq");

var cell_lines = text.split("\n");

for (var i=0; i < cell_lines.length; i++ ) {
lines.Append(cell_lines[i]);
}

table.SetText(cell, lines);
}
 


There may be strange issues if "zombie" or multiple Excel sessions are running. If Excel is clearly running, but unexpected error messages are received, it may be best to kill all Excel sessions, then start it over again.

Part 2: Sending drawing table data to Excel

Thursday, May 22, 2008

Pro/WebLink: Changing Model Relations Quickly with Wildfire 4

Changing relations is often a frustratingly difficult task to perform in a Pro/ENGINEER model. This is especially true for assemblies, even ones with only a few dozen parts.

Up until Wildfire 4, there were many ways to accomplish this involving some kind of file export/import process. These include mapkeys, J-Link, and Pro/WebLink, each having its own complications.

Starting with Wildfire 4, the PFC API's (Pro/WebLink, J-Link, VB API) have been enhanced to allow more direct access to relations. Relations can be read into a string sequence (a form of dynamic array), modified within the sequence, then re-applied to the model very quickly. This makes programmatic editing of relations very simple, fast, and easy.

While using the PFC API's to perform this action on a single part model may seem overkill compared to using mapkeys, consider that mapkeys don't scale well. If you're doing a one-off task, mapkeys work great. When you need to perform the task for all components of an assembly, whether large or small, the API's can accomplish the task much more quickly and reliably. For assemblies with thousands of components, mapkeys just won't get the job done.

What follows is an example of a Pro/WebLink function to add relations to a model from an array. First, existing relations are obtained from the model using the "Relations" property (a stringseq object), then elements of the array are appended to the stringseq using the Append() method. Finally, the modified stringseq is reassociated to the model. The change is immediate and does not require a regeneration.

function addRelations ( model, array ) {

// get relations as stringseq object
var rel_seq = model.Relations;

// append array elements to stringseq
for (var i=0; i<array.length; i++) {
rel_seq.Append(array[i]);
}

// assign modified stringseq to model relations
model.Relations = rel_seq;

}
 
Code to call the function is also pretty basic. Using the CurrentModel property of the session object, the model object can is obtained. Here an array is defined, but even better would be to pull the values from an HTML TextArea or some other data source. Lastly, the addRelations() function is called with the model object and the relations array.

// get current model
var session = pfcGetProESession();
var model = session.CurrentModel;

// define new relations as array
var array = new Array( "abc=123", "def=456" );

// add relations
addRelations(model, array);
 
A recursive procedure to apply relations to assembly components is only moderately more complicated. See my other examples only how to rcursively process assembly components with Pro/WebLink.




Comments and questions are always welcome, either here on my blog or via email at MarcMettes@InversionConsulting.com.

Tuesday, April 29, 2008

WebLink: Sending Pro/ENGINEER Point Data to Excel

Extracting point data from your Pro/ENGINEER models can be a cumbersome task, especially if you follow the suggested PTC export/import process. This can be streamlined significantly using Pro/WebLink, but also with J-Link and Pro/Toolkit.

I've discussed, in previous articles, obtaining lists of features and recursing through assemblies. What's different here is that a transformation matrix will be used to obtain the XYZ position of the point with respect to the default coordinate system of the assembly. This is the position data that will be output to Excel.


The HTML page has a single button, which initiates the point data extraction, and a single div field for some results. Library files pfcUtils.js and pnts2excel.js contain Javascript code. pfcUtils.js is a utility library provided by PTC. The code discuss in this article would be contained in the pnts2excel.js file.

<HTML>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="pfcUtils.js"></SCRIPT>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="pnts2excel.js"></SCRIPT>
<BODY>
<form name="f">
<INPUT name="a" type=button value="Get Point Data!" onclick="GetPoints()">
<br><div id="mesg"></div><br>
</form>
</BODY>
</HTML>

 

After the GetPoints() function has obtained the session object and verified that a model is active, it sets up an object used for some persistent data and for returning an array of points.

The object has the following properties: modelsFound (array), points (array), root (top-level model object), comppath_seq (intseq object) and transform (assembly tranformation matrix). GetPointData() is called using the current model and appdata object to obtain the point data. Then PntArrayToExcel() is used to send the data to Excel.

function GetPoints () {

if (!pfcIsWindows())
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

var elem = document.getElementById("mesg");

var modelname = "no model";
var session = null;
var model = null;

try {
session = pfcGetProESession();
}
catch (e) {
elem.innerHTML = "ERROR: Cannot connect to Pro/Engineer session!";
return;
}

try {
model = session.CurrentModel;
}
catch (e) {
// probably no model
elem.innerHTML = "Problem getting current model info.";
return;
}

elem.innerHTML = "<br>" + "Top Level Model: " + model.FileName;

// Create appdata object
//
var appdata = new Object();
appdata.modelsFound = new Array();
appdata.points = new Array();
appdata.root = model;
appdata.comppath_seq = new pfcCreate("intseq"); // need 'new', this is an instance object
appdata.transform = null;

GetPointData(model, appdata);
PntArrayToExcel(appdata.points);
}

 

There are two main actions in the GetPointData() function: point extraction, and recursing (for subassemblies). The modelsFound array is used in both actions, and helps to avoid extracting data from a model more than once. In the assignment statement, it is flagging the currently encountered model as having been processed, so that it will not get processed again.

After that, a sequence of points is obtained using the current model and the ListItem() method specifying ITEM_POINT. This will return all points in the model, not just a single feature, which is important if point array features are present in the model. Using the 'Point' property of the point, we can get a Point3D object which has the XYZ info. If there is a transform matrix available, it is applied to the point data first. The model name and XYZ data of the point are assigned to an object, which is then push into the 'points' array property of the appdata object.

In the recursing action, if the encountered model is a subassembly, the code iterates over the component features, if any. If the component is active, the feature id is appended to the comppath_seq sequence, which is used to get the model object from the feature object and the transformation matrix from the root assembly's default coordinate system. The matrix is saved into the appdata object.

If the component has not been encountered already, GetPointData() is called recursively with the component model info. After the function returns, the last element of the comppath_seq is removed.

function GetPointData ( model, appdata ) {

var elem = document.getElementById("mesg");
appdata.modelsFound[model.FileName] = 1;


// Get points in current model
//
var points = model.ListItems( pfcCreate("pfcModelItemType").ITEM_POINT );

for (var i = 0; i < points.Count; i++) {
var point = points.Item(i);
var pnt3d = null;

if (appdata.transform == null) {
pnt3d = point.Point;
}
else {
pnt3d = appdata.transform.TransformPoint(point.Point);
}

// send pnt data to the browser
elem.innerHTML += "<br> " + model.FileName + ": "
+ point.GetName() + " (Id " + point.Id + ")"
+ ", XYZ= ( "
+ pnt3d.Item(0) + ", "
+ pnt3d.Item(1) + ", "
+ pnt3d.Item(2) + " )"
;

var object = new Object();
object.Owner = model;
object.Point = pnt3d;

appdata.points.push(object);
}


// Recurse into components, if model is an assembly
//
if ( model.Type == pfcCreate("pfcModelType").MDL_ASSEMBLY ) {

var components = model.ListFeaturesByType( false, pfcCreate("pfcFeatureType").FEATTYPE_COMPONENT );

for (var i = 0; i < components.Count; i++) {

var compFeat = components.Item(i);

if (compFeat.Status != pfcCreate("pfcFeatureStatus").FEAT_ACTIVE) {
continue;
}

// Append id for use in building comppath
appdata.comppath_seq.Append(compFeat.Id);

try {
// Create ComponentPath object to get pfcModel object of component and transform
var cp = pfcCreate("MpfcAssembly").CreateComponentPath( appdata.root, appdata.comppath_seq );
var compMdl = cp.Leaf;
appdata.transform = cp.GetTransform(true);
} catch (e) {
elem.innerHTML += "<br> CreateComponentPath() exception: " + pfcGetExceptionType(e);
}

// Descend into subassembly
if ( !(compMdl.FileName in appdata.modelsFound) ) {
GetPointData(compMdl, appdata);
}

// Remove id (last in seq), not needed anymore
try {
appdata.comppath_seq.Remove( (appdata.comppath_seq.Count-1), (appdata.comppath_seq.Count) );
} catch (e) {
elem.innerHTML += "<br> comppath_seq.Remove exception: " + pfcGetExceptionType(e);
}

} // Loop: components

} // model.Type
}

 

The PntArrayToExcel() function sends the data to Excel. The code first tries to use an existing Excel session, but will start a new one if necessary. Certain IE security settings may result in a new session being started every time.

Once an Excel session is available and a new workbook has been created, the code iterates over the 'points' array property of the appdata object to write the data into the active sheet. Four columns are used in the output, which include the model name, X position, Y position, and Z position. Since a particular coordinate system was not referenced, the default coordinate system of the top-level assembly is used.

function PntArrayToExcel ( array ) {

var oXL;
var elem = document.getElementById("mesg");

// Get/Create Excel Object Reference
try {
oXL = GetObject("","Excel.Application"); // Use current Excel session
}
catch (e) {
// couldn't get an excel session, try starting a new one
try {
oXL = new ActiveXObject("Excel.Application"); // Open new Excel session
}
catch (e) {
// couldn't start a new excel session either
}
}

if (oXL == null) {
elem.innerHTML = "Could not get or start Excel session!";
return;
}

try {
oXL.Visible = true;
var oWB = oXL.Workbooks.Add();
var oSheet = oWB.ActiveSheet;
}
catch (e) {
elem.innerHTML = "Problem creating new workbook.";
return;
}

for (var i=0; i < array.length; i++ ) {
var pnt3d = array[i].Point;
var ownerMdl = array[i].Owner;

oSheet.Cells(i+1, 1).Value = ownerMdl.FileName;
oSheet.Cells(i+1, 2).Value = "" + pnt3d.Item(0);
oSheet.Cells(i+1, 3).Value = "" + pnt3d.Item(1);
oSheet.Cells(i+1, 4).Value = "" + pnt3d.Item(2);
}
}

 

Other than the transformation matrix, the code is pretty straightforward and easily adaptable to other data sets (i.e. parameters, layers, feature lists, etc).


Questions and comments are always welcome, either here on my blog or at MarcMettes@InversionConsulting.com.

Saturday, April 26, 2008

Pro/WebLink: Sending Your Pro/ENGINEER Assembly BOM to Excel

One question I read frequently on the forums is about how to get BOM data of a Pro/ENGINEER assembly into Excel. Typically the solutions involve saving files to disk, then some editing, and finally reading that data into Excel.

This example will demonstrate how to skip these extra steps and, using Pro/WebLink, send your BOM directly from Pro/ENGINEER into Excel.

The HTML Page

The starting point is this very simply HTML page. At the beginning, it pulls in two JavaScript libraries, pfcUtils.js and bom2excel.js. As mentioned in my previous Pro/WebLink article, pfcUtils.js is a small PTC provided library. bom2excel.js will contain the remaining JavaScript code mentioned in this article.

The HTML page also contains two buttons and two div fields. The two div fields are "buckets" used for output and status messages and will contain HTML code added programmatically. One button initiates the action and the other clears the div fields.

<HTML>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="pfcUtils.js"></SCRIPT>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="bom2excel.js"></SCRIPT>
<BODY>

<form name="f">

<br><INPUT id="get_btn" type=button value="Get BOM" onclick="GetData()">
<INPUT id="clr_btn" type=button value="Clear" onclick="Clear()">
<br><div id="data"></div><br>
<br><div id="status"></div><br>

</form>

</BODY>
</HTML>


 

The Initialization Function

The GetData() function initializes the data structures, gets the BOM data using the recursive GetBOMData() function, and sends the data to Excel or the browser using the SendData() function.

Once we're sure that we're connected to a Pro/ENGINEER session properly and a model is active, the function sets up an object that will be used by the recursive GetBOMData() function. The properties of this object are "params", "comppath_seq" and "root".

The params property lists the columns that will appear in the output. Three of the columns ("LEVEL", "NAME", and "QTY") are special and have supporting code to populate their values. All others are presumed to be Pro/ENGINEER parameters and are treated as such.

The comppath_seq and root properties are used to transform component feature objects into model objects via the pfcComponentPath class.

When the appdata object has been setup, it is passed to GetBOMData, which returns an array of "model arrays". Each "model array" contains information about each part or assembly that was encountered in the BOM. This array of arrays is assigned to the "values" property of the appdata object.

The object is then passed to SendData(), which will attempt to put the data into Excel.

function GetData () {

if (!pfcIsWindows())
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

var data_elem = document.getElementById("data");
var session = null;
var model = null;

// Get session object
try { session = pfcGetProESession(); }
catch (e) {
data_elem.innerHTML = "ERROR: Cannot connect to Pro/Engineer session!";
return;
}

// Make sure there is a model active
try { model = session.CurrentModel; }
catch (e) {
// probably no model
data_elem.innerHTML = "Problem getting current model info.";
return;
}

data_elem.innerHTML = "<br>" + "Top Level Model: " + model.FileName;

// Setup appdata object for bom data
var appdata = new Object();
appdata.params = new Array( "LEVEL", "NAME", "QTY", "DESC", "PROI_CREATED_ON" );
appdata.comppath_seq = new pfcCreate("intseq");
appdata.root = model;

// get bom data as an array of arrays
appdata.values = GetBOMData(model, appdata);

// send bom data
SendData(appdata);
}



 

The Recursive Function

The GetBOMData() function recursively gathers the BOM data for an assembly. There are three main actions performed in this function: attribute gathering, recursing (for subassemblies), and quantity adjustments.

Before the current model attribute gathering, the parent name of the currently encountered component is stored. The logic used here flattens the tree structure of the assembly into a single array. In order to adjust the quantity, the parent needs to be tracked in order to adjust the quantity for the current level only.

In the attribute gathering code, you'll see code handling the three special attributes: level, name, and qty. Name is simply the model name. Qty is used here only for the top-level object, which always has a quantity of one. Level is calculated from the comppath_seq property. The ComponentPath is essentially an array of feature id's that let you walk through the assembly structure to a specific component. The length of the array indicates the component level in the assembly.

Any other items encountered in the params property of the appdata object is assumed to be a Pro/ENGINEER parameter and the GetParam() method is used to obtain its object. A try block handles the situation where there is no parameter of that name and a default value is used instead.

In the recursing section, which is skipped if the encountered model is a part, the code loops through all of the assembly components. The are four main actions performed in the loop. First addressed is building the ComponentPath, by appending the component's feature id, which gives the pfcModel object of the component. Second is determining whether to recurse, and handling the resulting arrays if it does. Components are not processed more than once at a given level. In the third action, the quantity count is initialized, if necessary, and incremented. Finally, the component id is removed from the comppath_seq.

The final task in GetBOMData() is to adjust the quantity. This is done by looking up component names in the qtyCount associative array. This is done only for components returned from recursive calls, which explains why the loop starts at index 1 not 0. A component cannot know how many times it is assembled. This can only be known from the subassembly level.

Finally, the array of model_arrays is returned back the previous level.

function GetBOMData ( model, appdata ) {

var data_elem = document.getElementById("data");
var status_elem = document.getElementById("status");

var model_array = new Array(); // data for this model
var return_array = new Array(); // array to store model_array's

// Assign parent attribute for qty count
//
try {
model_array["PARENT"] = appdata.parent.FileName;
}
catch (e) {
// ignore exception, probably top-level asm
model_array["PARENT"] = "";
}


// Get params of current model
//
for (var i = 0; i < appdata.params.length; i++) {

if (appdata.params[i] == "LEVEL") {
model_array["LEVEL"] = appdata.comppath_seq.Count+1;
}
else if (appdata.params[i] == "NAME") {
model_array["NAME"] = model.FileName;
}
else if (appdata.params[i] == "QTY" && model == appdata.root) {
model_array["QTY"] = 1;
}
else {
var param = null;
var paramvalue = " -- n/a -- ";

try {
// get parameter object
param = model.GetParam(appdata.params[i]);

// get parameter value
switch (param.Value.discr) {
case pfcCreate("pfcParamValueType").PARAM_STRING:
paramvalue = param.Value.StringValue;
break;
case pfcCreate("pfcParamValueType").PARAM_INTEGER:
paramvalue = param.Value.IntValue;
break;
case pfcCreate("pfcParamValueType").PARAM_BOOLEAN:
if (param.Value.BoolValue)
paramvalue = true;
else
paramvalue = false;
break;
case pfcCreate("pfcParamValueType").PARAM_DOUBLE:
paramvalue = param.Value.DoubleValue;
break;
}
}
catch (e) {
// param probably doesn't exist, ignore
}

// store param value in model array
model_array[appdata.params[i]] = paramvalue;
}
}

// store model array in return array
return_array.push(model_array);


// Recurse into components, if model is an assembly
//
if ( model.Type == pfcCreate("pfcModelType").MDL_ASSEMBLY ) {

var compMdl = null;
var qtyIndexName = null;
var qtyCount = new Array();

// get component sequence of current subasm
var components = model.ListFeaturesByType( false, pfcCreate("pfcFeatureType").FEATTYPE_COMPONENT );

// loop through components
for (var i = 0; i < components.Count; i++) {

var compFeat = components.Item(i);

if (compFeat.Status != pfcCreate("pfcFeatureStatus").FEAT_ACTIVE) {
continue; // skip inactive components
}

// Append component id to sequence (for building ComponentPath)
appdata.comppath_seq.Append(compFeat.Id);

// get model object of component
try {
// have to create ComponentPath object first, then use "Leaf" property
var cp = pfcCreate("MpfcAssembly").CreateComponentPath( appdata.root, appdata.comppath_seq );
compMdl = cp.Leaf;
} catch (e) {
status_elem.innerHTML += "<br> CreateComponentPath() exception: " + pfcGetExceptionType(e);
}

// using a unique index (subasm & comp names) for the qty count array
qtyIndexName = model.FileName+"/"+compMdl.FileName
appdata.parent = model;

// Descend into subassembly, if model has not been processed in this subasm
if ( !(qtyIndexName in qtyCount) && compMdl != model ) {
// concatenated arr into return_array (concat doesn't seem to work)
var arr = GetBOMData(compMdl, appdata);
for (var j=0; j<arr.length; j++) {
return_array.push(arr[j]);
}
arr = null;
}

// initialize and increment qty count for this subasm/component
if ( ! (qtyIndexName in qtyCount) ) {
qtyCount[qtyIndexName] = 0;
}
qtyCount[qtyIndexName]++;


// Remove last id in sequence, not needed anymore
try {
appdata.comppath_seq.Remove( (appdata.comppath_seq.Count-1), (appdata.comppath_seq.Count) );
} catch (e) {
status_elem.innerHTML += "<br> comppath_seq.Remove exception: " + pfcGetExceptionType(e);
}

} // Loop: components


// process arrays (for qty adjust) returned from GetBOMData() call
for (var i = 1; i < return_array.length; i++) {

var compName = return_array[i]["NAME"];
qtyIndexName = model.FileName+"/"+compName;

// Adjust qty for current level objects
if (return_array[i]["PARENT"] == model.FileName) {
for (var j = 0; j < appdata.params.length; j++) {

// make sure qty was requested
if (appdata.params[j] == "QTY") {
if (qtyIndexName in qtyCount) {
return_array[i]["QTY"] = qtyCount[qtyIndexName];
}
else {
return_array[i]["QTY"] = 1;
}
}

}
}
}

qtyCount = null;

} // model.Type

return return_array;
}



 

The Sending Function

The SendData() function is used to send the data to Excel (Windows) or to the browser (Unix).

On Windows, the code gets an Excel session object, either from an existing session or by starting a new one, if necessary. Your IE security settings may cause a new session to be started every time. A new workbook is created, and the data is written to the cells, headers first, then data rows.

The column header values are pulled from the params property array of the appdata object. These values are used to look up values in each model_array from the values property. You'll note that Excel cell indexes start at 1 and not 0 as with the JavaScript arrays.

On Unix, the data is written to the "data" div field on the HTML page, also using the params property for the headers and values property for the parameter values.

function SendData ( appdata ) {

var oXL = null;
var data_elem = document.getElementById("data");

if (appdata.values.length == 0) {
data_elem.innerHTML = "No data to send!";
return;
}

if (pfcIsWindows()) {

// Get/Create Excel Object Reference
try {
oXL = GetObject("","Excel.Application"); // Use current Excel session
}
catch (e) {
// couldn't get an excel session, try starting a new one
try {
oXL = new ActiveXObject("Excel.Application"); // Open new Excel session
}
catch (e) {
// couldn't start a new excel session either
}
}

if (oXL == null) {
data_elem.innerHTML = "Could not get or start Excel session!";
return;
}

// Create new workbook
try {
oXL.Visible = true;
var oWB = oXL.Workbooks.Add();
var oSheet = oWB.ActiveSheet;
}
catch (e) {
data_elem.innerHTML = "Problem creating new workbook.";
return;
}

// Write header cells
for (var i=0; i < appdata.params.length; i++ ) {
oSheet.Cells(1, i+1).Value = appdata.params[i];
}

// Write data cells
for (var i=0; i < appdata.values.length; i++ ) {
for (var j=0; j < appdata.params.length; j++ ) {
oSheet.Cells(i+2, j+1).Value = appdata.values[i][appdata.params[j]];
}
}
}
else {

// Not a windows platform, write data to browser

// Write header cells
data_elem.innerHTML += appdata.params.join(" &nbsp; / &nbsp; ");

// Write data cells
for (var i=0; i < appdata.values.length; i++ ) {
data_elem.innerHTML += "<br>";
for (var j=0; j < appdata.params.length; j++ ) {
if (j > 0) { data_elem.innerHTML += " / "; }
data_elem.innerHTML += appdata.values[i][appdata.params[j]];
}
}

}

}


 

The Cleanup Function

The Clear() function is very simple. It just blanks the content div fields.

function Clear() {
var data_elem = document.getElementById("data");
var status_elem = document.getElementById("status");
data_elem.innerHTML = "";
status_elem.innerHTML = "";
}


 

The code is somewhat more complex than I had expected, but this is largely due to the quantity adjustment. Strip out this and the code is signifcantly more terse, but less functional of course. I have a enhanced version of this application that gets the attribute data from a list in a textfield. This is a bit more practical because it allows for changes at runtime without having to edit the code. If there is interest, I will discuss those changes.


Questions and comments are always welcome, either here on my blog or at MarcMettes@InversionConsulting.com.

Sunday, April 13, 2008

WebLink: Sending Data from Pro/ENGINEER to Microsoft Excel with JavaScript

Occasionally data is needed in Microsoft Excel from Pro/ENGINEER, whether it is geometry values, parameter values, or BOM table contents. Data from any of these data sources can be sent directly to Excel with Pro/WebLink, without writing any external CSV files and without running any other applications.

Technically speaking it is JavaScript functionality, or JScript as Microsoft likes to call it, and not really Pro/WebLink at all. To be useful though, it will be running within the context of a Pro/WebLink application.

Listed below is a function that takes an array of (number or text) values and writes the data in a new Excel workbook. You'll probably find many examples on the Internet using Visual Basic having the same basic steps.

The most important part is getting the handle to an Excel session. The "new ActiveXObject()" call will start a new session of Excel, while the GetObject() call will obtain a handle to an existing Excel session. Depending on your Internet Explorer security settings (i.e. "Initialize and script ActiveX controls not marked safe for scripting"), you may have to use one or the other, but ideally both should work. Using an existing session is definitely more useful when sending data from Excel to Pro/ENGINEER.

After the handle is obtained, the session is setup to be visible with a new workbook (.xls file). A reference is then obtained to the active sheet. Using the "Value" property of a specific cell in the active sheet, we can put data into the cell, in this case from the array passed to the function.

function arrayToExcel ( array ) {
var oXL;

try {
oXL = new ActiveXObject("Excel.Application"); // Use new session
// oXL = GetObject("","Excel.Application"); // Use existing session
}
catch (e) {
alert("Excel must be running!");
return;
}

try {
oXL.Visible = true;
var oWB = oXL.Workbooks.Add();
var oSheet = oWB.ActiveSheet;
}
catch (e) {
alert("Problem creating new workbook.");
return;
}

for (var i=0; i < array.length; i++ ) {
oSheet.Cells(i+1, 1).Value = array[i];
}
}

Here is some example code that populates an array and calls the "arrayToExcel()" function:
var array = new Array();
array.push(1.11);
array.push(2.22);
array.push(3.33);
arrayToExcel(array);

As always, comments and questions are welcome.

Wednesday, April 9, 2008

WebLink: Hello World!

The Classic First Example Program for WebLink

Getting started with WebLink is a challenge because there aren't many examples provided by PTC. There are a few good examples in there, but they represent only a small fraction of the API. This together with the sometimes complex browser security issues can make WebLink seem unapproachable.

In the interest of getting you up and running with WebLink, I'll discuss some of the major hurdles and provide a good, basic starting point.

On Windows, the Internet Explorer browser security model may require the following:

  • WebLink HTML page must be served by a web server
  • URL may need to be fully qualified (i.e. http://srv1.yourcompany.com/... not just http://srv1/...)
  • Internet Explorer should consider your web server as a "trusted host"
  • config.pro option WEB_ENABLE_JAVASCRIPT must be set to ON


For WebLink that may be enough, but if using other COM objects, such as interfacing with MS Excel for example, changes to IE security settings may be required to grant your application more privileges. This is also a security risk, so be careful when doing this.


The Hello World example is self contained other than loading the (PTC provided) pfcUtils.js file. This file can be placed in the same folder on the web server as the HTML file. The example has a small form containing an anchor tag (with id of "mesg") and a button. The button executes the HitMe() function which populates the contents of the anchor tag.

In nearly every application the pfcGetProESession() function is called, which is contained in the pfcUtils library file. The try/catch block around it verifies that the embedded browser is used, which is essential.

Once we have the session object, the current model object is obtained, from which we can get the name of the model. This value is displayed along with a message in the anchor tag. The try/catch block here helps verify that there is an active model (part, assembly, or drawing), because "model" will be null if there isn't. Trying to call any method against null is pretty much guaranteed to throw an exception.

WebLink Hello World Example:

<HTML>
<SCRIPT LANGUAGE="JavaScript" type=text/javascript src="pfcUtils.js"></SCRIPT>
<BODY>

<SCRIPT LANGUAGE="JavaScript">

function HitMe() {

if (!pfcIsWindows())
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

var form_elem = document.getElementById("mesg");

var modelname = "no model";
var session = null;
var model = null;

try {
session = pfcGetProESession();
}
catch (e) {
form_elem.innerHTML = "ERROR: Cannot connect to Pro/Engineer session!";
return;
}

try {
model = session.CurrentModel;
modelname = model.FileName;
}
catch (e) {
// probably no model
form_elem.innerHTML = "Make sure a model is active!";
return;
}

form_elem.innerHTML = "Hello! My name is: " + modelname;

}

</SCRIPT>

<form name="f">
<INPUT name="btn1" type=button value="Hit me!" onclick="HitMe()">
<br><a id="mesg"></a>
</form>
</BODY>
</HTML>

If you have any questions, please ask. Comments and questions are welcome.

Tuesday, April 8, 2008

WebLink: What Is It Anyway?

A discussion of what WebLink is, and is not

WebLink is one of the two free API's (JLink being the other) provided by PTC to create Pro/Engineer applications. According to PTC's documentation, WebLink is a Javascript based library for use within the embedded web browser.

That's sounds great, but it's about as accurate as describing a car as a pothole creator. Well, here in Detroit that's more true than not, especially on Van Dyke ... but I digress.

Truthfully, WebLink isn't really Javascript based at all. It's based on Microsoft's COM/ActiveX/OLE objects on Windows, and Mozilla's XP-COM on Unix. You don't even need to use Javascript. On Windows, you can use VBScript, Perl, Python, or any one of your favorite languages that can access COM objects.

Here's an example of using VBScript in a WebLink application:

<HTML><BODY>
<SCRIPT type="text/vbscript">
function HitMe ()
dim mdlname
Set pfccomglob = CreateObject("pfc.MpfcCOMGlobal")
mdlname = pfccomglob.GetProESession.CurrentModel.FileName
Document.getElementById("mdlname").innerHTML = "Name: " & mdlname
msgbox("Name: " & mdlname)
end function
</SCRIPT>
<form name="f">
<INPUT name="a" type=button value="Hit me!" onclick="HitMe()">
<div id="mdlname"></div>
</form>
</BODY></HTML>

To make matters worse, you don't even need to use the web browser! A Pro/Toolkit DLL can access the WebLink COM objects using C/C++ and can do so completely outside the confines of the web browser. The Pro/Toolkit DLL could also go as far as hosting your favorite scripting engine. The web browser is just one of many environments in which a WebLink application can run.

Here's the same example using Perl to access WebLink COM Objects from within a Pro/Toolkit DLL:
use Win32;
use Win32::OLE;
$pfccomglob = Win32::OLE->new('pfc.MpfcCOMGlobal');
$mdlName = $pfccomglob->GetProESession->CurrentModel->FileName;
Win32::MsgBox("Name: $mdlName");

To summarize, WebLink requires neither Javascript nor a web browser. That clears things up, right? Why is it called "WebLink"? Well, you can use it in a web browser, and all PTC software has to have the word "Link", as a result: WebLink!

Can you imagine the response had they called it Pro/COM, or ActiveXLink, or COM.Link? A resounding "huh?" would have echoed through the halls on Kendrick Street. I think the marketing guys got it right this time.

Anyway, using whatever language in whatever environment you desire, the huge benefit to WebLink is that it's a rapid prototyping system for Pro/Engineer applications. The code-test-code-test cycle can be repeated as quickly as you can refresh the web browser. Whether you intend to migrate the application to JLink, or leave it in the web browser, you can get a complex application written very, very quickly. That's what makes WebLink very powerful.

Learn it, you won't regret it.

Friday, March 21, 2008

Optimizing WebLink: A Better pfcCreate()

While performance testing a processing intensive WebLink application in Pro/Engineer, I came across something surprising. Use of the PTC supplied pfcCreate() function inside a loop made the application significantly slower.

The bigger the loop, the slower the application. For a small loop, the user probably wouldn't notice, but it is very noticeable when recursively processing 10,000+ part assemblies.

Here's what I was using that could be very slow:

for (int i=0; i<array.Count; i++) {
obj = pfcCreate("pfcBlahBlahBlah");
// do something here with 'obj'
}






I came to the conclusion that getting these objects from the COM server (on windows) was an expensive operation. Minimizing its use seemed to be a very good idea. My first though was to simply pull pfcCreate() out of the loop, which made it much faster. Problem solved!

Faster code:

obj = pfcCreate("pfcBlahBlahBlah");

for (int i=0; i<array.Count; i++) {
// do something here with 'obj'
}







Well, not quite.

The problem is that that chunk of code is used in some other loop, which could be used in some other loop, possibly in some other function, and so on. It was just not enough to rewrite the code in that way. Something else had to be done to eliminate that performance hit.

When it occurred to me that pfcCreate() always returned class objects - the same every time - then I had my answer. Rewrite pfcCreate() and enable caching of the returned objects.

Now, instead of hitting the COM server for every request, the function first performs a lookup of the class object in a global array (creating it if necessary). If the class object has previously been requested, it will be found in the global array, and that object will be returned. If it is not found, the class object is obtained from the COM server, stored in the global array, and then returned to the caller.

In addition to being more efficient when getting WebLink class objects, I now can write my code any way I want putting pfcCreate() inside or outside of the loop, no performance hit either way.


Here is the modified pfcCreate() that I use in my applications:


function pfcCreate (className) {
if (!pfcIsWindows())
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

if (className.match(/^M?pfc/)) {

// Check global object cache first, then return that object
//
try {
if (className in global_class_cache) {
return global_class_cache[className];
}
}
catch (e) {
// Probably no global_class_cache yet
global_class_cache = new Object();
}

}

// Not in global object cache, create object
var obj = null;

if (pfcIsWindows()) {
obj = new ActiveXObject("pfc."+className);
}
else {
obj = Components.classes ["@ptc.com/pfc/" + className + ";1"].createInstance();
}

// Return created object
//
if (className.match(/^M?pfc/)) {
global_class_cache[className] = obj;
}

return obj;
}