Shape Script made easy
Shape Script made easy
Thomas Kilian
Buy on Leanpub

1. Preface

Sometimes – or even often – you need different shapes than those from standard UML. That is something resembling a technical device rather than a rectangle or a stickman. In that case Enterprise Architect1 offers a nice feature which is called Shape Script. As the name suggest you will find a scripting language which allows you to define the shape of elements and even connectors.

This book is intended as tutorial and reference for the Shape Script language. It offers a step by step introduction, a lot of examples and quite some tricks you need to know when using Shape Scripts.

Anyhow, a common use of Shape Script is in combination with MDG Technology files. Any stereotypes defined therein come along with a number of stereotype properties (aka. tagged values). These can be used to show different shapes and/or text in the element. Quite some MDGs being delivered with Enterprise Architect use this technique.

Also all of the information in this book has been tested by me in many circumstances I can not hold any liability for use of the here presented information2. However, I’d be glad to receive any kind of feedback to correct future updates of this book which you will receive for free in turn. Having said this, all information presented here is subject to change without notice.

The names of actual companies and products mentioned herein may be the trademarks of their respective owners.

3. Basic Concept

Shape Scripts in Enterprise Architect are a way to assign individual shapes to stereotyped elements and connectors. The language is somewhat C-like but is limited to quite a small subset of control structures and it offers a number of build-in graphic drawing methods. The advantage is that you can learn it rather fast. The drawback is that you soon reach limits when trying to do more advanced graphics. This however should be acceptable as elements should not be complex graphic art but meaningful symbols.

We will start with shapes for elements before explaining connectors. An element shape has a size of 100 units in width and height. The top left (X|Y) coordinate is (0|0) and the bottom right coordinate is (100|100). These coordinates will be scaled to whatever you size the element on a diagram.

Before going into details of the language itself let us try a simple example. Open Settings/UML Types/Stereotypes in any temporary Enterprise Architect repository you would like to use as sandbox.

Then create a new stereotype by entering ae3 in the Stereotype field and choosing <all>4 from the Base Class drop down. That will allow to assign the stereotype to all elements and see what the shape will look like for them.

Now the important part: check the Shape Script radio button and click Assign5. This will open the Shape Script editor where you can type the script in the left pane. By clicking Refresh you will see the resulting image in the right one6.

7

While typing the opening bracket after the moveto and lineto you will notice that the parameter list for the method is shown as balloon help.

Further when hitting Ctrl-Space at any time the editor will open a drop down with possible methods starting with the characters already typed for the current word (top of the list if at new line or at a space):

Once you have saved the script in the editor and the ≪ae≫ stereotype as such you may apply the stereotype to see how it looks like on a real diagram. This may differ from the preview in certain cases. Also you can test scaling and later then conditional drawings.

The result looks a bit dull. But it shows the basic principle how to assign and test Shape Scripts.

3.1 Control Structures

As already mentioned Shape Script is very limited in its capabilities. So are control structures. You only have if and else to control the flow of statements. There are no loops at all.

The format for that is:

if (<query>) <block-or-statement>
where <query> is one of the methods described below. <block-or-statement> is either a sequence of the graphic methods described above enclosed in { and }-braces or a single statement . And of course any if-construct counts as statement as well.
if (<query>) <block-or-statement> else <block-or-statement>
is hopefully obvious. Syntactically you can abbreviate if-cascades by using if (<query>) <block-or-statement> else if (<query>) <block-or-statement> ...
return
is a single statement that may appear at any position. It will stop further processing of the Shape Script immediately. You can use this statement to mimic a case statement instead of using an if-cascade.

3.2 Query Methods

The use of queries is a more advanced8 feature and will be used later in chapter Advanced Usage. So here’s just a general overview of the operations.

HasTag(tagName)
will evaluate true if the tagged value named tagName exists at all.
HasTag(tagName,tagValue)
will evaluate true if the tagged value named tagName exists and has a value equal to tagValue.

The values for property in the two operations below are explained in chapter Properties. This and the parameter value must be supplied as string, i.e. enclosed in either single or double quotes.

HasProperty(<property>)
will evaluate true if the property named property exists at all. E.g. HasProperty("alias"); will evaluate true only of a alias has been defined in the properties.
HasProperty(<property>,<value>)
is the same as the previous method except that it checks for equivalence of value and the result of property. So you could check if an element is named specifically (which only makes limited sense).

4. Shaping Elements

As already mentioned the Shape Script language is a bit C-like. So probably most people will not have much trouble to learn the syntax. Anyway it’s very limited. An EBNF syntax description can be found in the appendix.

Generally all keywords and even strings are case insensitive. So it does not matter whether you write LineTo rather than lineto in the above example. The auto-completion suggests the first variant in camel case which is definitely better to read.

4.1 The Main Shape

Or to talk Sparxian: shape main. As you already have seen, these two keywords introduce the body wrapped in curly brackets. The instructions inside will be executed each time a stereotyped element is shown on a diagram. As already mentioned each element has 100 units in width and depth (as opposed to height since the units increase downwards). Even shapes which appear oval (like Use Case) have that rectangular 100² units frame.

Now let’s see what can be done to actually draw something. Let’s start with the two methods LineTo and MoveTo used in the introductory example.

Simple Lines

There is not much to say:

MoveTo(x,y)
moves the graphic cursor to the specified coordinate. Initially in the script the cursor is located at (0|0).
LineTo(x,y)
draws a line from the current coordinate to the new (x|y) and sets the current cursor to that position.

A somehow more advanced way to draw a line is a bezier curve.

BezierTo(xStart,yStart,xBend,yBend,xEnd,yEnd)
draws a line from (xStart|yStart) to (xEnd|yEnd) bending it towards (xBend|yBend)

Other ways to get some rounded shapes are the Arc and ArcTo methods:

Arc(left,top,right,bottom,xStart,yStart,xEnd,yEnd)
will draw a partial ellipse (see Ellipse below in the next chapter) with the bounding specified by left, top, right and bottom. But it is only drawn from the intersection of the line crossing the ellipse which is drawn from its center to (xStartX|yStart) to that of the intersection center/(xEnd|yEnd).
ArcTo(left,top,right,bottom,xStart,yStart,xEnd,yEnd)
will draw the same as Arc except it will continue a straight line from the current pen position to the start of the arc and it will set the current pen position to the end of the arc (which is the counter-clockwise end)

Closed Lines

In contrast to the simple lines explained above the closed lines have an inside which can be filled with a solid color. There are different ways to create such a graphic element.

Rectangle(left,top,right,bottom)
will do as the name suggests where the edges are located at left,top,right and bottom.
RoundRect(left,top,right,bottom,cornerWidth,cornerHeight)
is almost the same as Rectangle but it will have rounded edges. The rounding is not related to the internal 100² units[^square] but given in absolute pixels! That means it will not scale with the shape as you can see below with the two differently sized shapes.
Polygon(xCenter,yCenter,numberOfSides,radius,rotation)
will draw a polygon with numberOfSides sides. The center will be at (xCenter|yCenter) and the radius (distance from center to an edge) is radius. Further the whole object will be rotated rotation degrees counter-clockwise. While all other parameters are integers, rotation is a float. Makes perfect sense to distinguish half a degree in 100² units.
Ellipse(left,top,right,bottom)
draws an ellipse inside the specified boundaries. For good reasons there is no Circle method as resizing the element on the diagram will in almost all cases distort it to an ellipse.

The following picture shows a number of example shapes.

The pentagon drawn in line 3 has the same radius as the (circular) ellipse drawn on line 2. The ellipses on line 4 and 5 mark the center and the right (and start-) margin of the pentagon.

A similar pentagon as on line 3 is drawn on line 7 except that it is rotated by 90 degrees counter-clockwise (and it is located at the right end of the shape).

Lines 8 and 9 show a triangle and a hexagon – hopefully self-explanatory.

There is also a way to draw non-regular shapes by specifying the edges and filling the contents.

StartPath()
introduces a sequences of MoveTo and LineTo commands describing the shape to be filled.
StartCloudPath(puffWidth,puffHeight,noise)
is almost the same as StartPath but it will make the shape cloudy. If you pass an empty parameter list EA will take some default values which mostly resemble a cloud. A value tuple of 40|20 for puffWidth|puffHeight will give a summer time cloud. Small values like 4|2 will result more in ripped paper shapes. Suppling zero as noise will result in absolutely regular fluffs. A value of about 1 will make it naturally irregular. All values can be supplied as float. Any integer will suffice however.
EndPath()
tells that the path is ended and going to be filled.
FillAndStrokePath()
Will fill the inside of the previously specified path and draw a border line.
FillPath()
is almost the same except it does not draw the border line.
StrokePath()
is another quite useless method as it just draws the border. Something you would achieve when simply not using all that path-stuff.

Finally there is a way to draw the native shape which will normally be used inside control structures (as we will see later).

DrawNativeShape()
will draw exactly the shape as if no Shape Script were applied.

The example above shows the preview and how a class will appear in the diagram. Since the preview does not know anything about the element where the Shape Script will be applied it will not draw anything for DrawNativeShape.

Painting

All of the above methods use default fill, border and font colors and border style. These can be changed using the following methods.

SetFillColor(red,green,blue)
replaces the fill color for the methods described in the previous chapter. The parameters take the RGB values as integer in the range from 0 to 255.
SetFillColor(newColor)
is the same as the previous except that it takes a color object as parameter. Currently9 there are just three methods to deliver a color object: GetUser***Color() (see below). You can not (yet?) define any color object in Shape Script.
SetFontColor(red,green,blue)
replaces the color for any text appearing in a shape.
SetFontColor(newColor)
is the same as the last method. Like for the above describe newColor you can only use one of the GetUser***Color() methods.
SetLineStyle( lineStyle)
sets the line style according to the lineStyle supplied as string. The string itself is interpreted case insensitive. You need to choose from one of the values solid, dash, dot, dashdot or dashdotdot. Choosing a different string value will result in an unpredictable line style
SetPen(red,green,blue)
replaces the color for border lines with the one supplied as RGB.
SetPen(red,green,blue,penWidth)
is the same as the previous one except that the additional parameter penWidth will set the width of the lines being drawn. The value for it must be between 1 (default thin line) and 5 (thickest line).
SetPen(newColor)
see the remark about color objects above.
SetPen(newColor,penWidth)
ditto
SetPenColor(red,green,blue)
yet another way to say SetPen(red,green,blue).
SetPenColor(newColor)
I must not repeat myself, must I?
SetPenWidth(penWidth)
just like SetPen(red,green,blue,penWidth) but without changing the color.
SetDefaultColors()
will reset any of the previously changed color attributes to the defaults.
GetUserFillColor()
returns a color object with the user defined color for use in one of the previously mentioned color parameters.
GetUserBorderColor()
ditto for the border
GetUserFontColor()
and for the font color.
GetUserPenSize()
returns the width (why be orthogonal?) of the pen as defined by the user. The only place where you can use this method is inside SetPen/SetPenWidth.

As you can see the colors shown in the painter for border, font and fill are used to color the three rectangles. Additionally the lower right rectangle is drawn with a thick border.

4.2 Text

Of course you need to be able to put some text inside a shape. The following methods can be used for that purpose. Note that any text goes into the whole active shape rectangle, which is shape main. So currently you will only be able to print text from top left to bottom right of the shape. We will see later how to place text at certain positions.

Print(text)
Renders the string text at the current print position (which starts top left of the actual shape).
Println(text)
is the same except that it renders a new-line at the end of the supplied text string.
PrintWrapped(text)
actually the same as Print. According to the help: Prints the specified text string, wrapped over multiple lines if the text is wider than its containing shape. Let’s see:

And the difference is —

None. Both texts are wrapped inside the shape. Further the text protrudes the shape on the bottom. Regardless of which Print variant is used.

PrintIfDefined(propertyname,truePart,falsePart)
will print the string truePart if the property is defined and has a not-empty value assigned. Else it will print falsePart.

Strings

The above mentioned Print methods and some of the methods described in the following take a string parameter. Since Shape Script is very limited you can not manipulate strings. To overcome this in a certain extent you can let Shape Script replace strings by some meaningful contents. That is e.g. the name of the element in place or the contents of tagged values. The mechanism is very simple. Shape Script interprets hash-tags and replaces them textually. E.g. the hash-tag #name# will be replaced by the name of the element. There is quite a number of properties you can use instead of name which will be covered in detail later in chapter Properties.

4.3 Shape Attributes

Before going on with more sophisticated methods we need to look at shape attributes. Some of these have global influence on the shape while others influence the behavior of certain methods. The general format for an attribute assignment is

<attribute> = <value>
where <attribute> is one of the names below and <value> is either a string (e.g. "string"), and integer or a tuple (e.g. (0,5)).
bottomAnchorOffset
is a tuple defining the offset for an embedded element related to the bottom of the element. E.g. a port will be (-7,-7) while a part will be (0,0). This offset is applied for embedded elements attached to the bottom of the parent element.
topAnchorOffset, leftAnchorOffset, rightAnchorOffset
are analogously attributes for the other edges.
dockable
is either standard or off.
editableField
Defines a shape as an editable region of the element. Valid Values: alias, name, note and stereotype.
fixedAspectRatio
either true or false. In the first case the element will scale always proportionally in width and height.
h_Align
Affects horizontal placement of printed text and sub-shapes (see next chapter) depending on the layoutType attribute. Valid values: left, center or right.
v_Align
Affects vertical placement of printed text and sub-shapes (see next chapter) depending on the layoutType attribute. Valid values: top, center or bottom.
layoutType
Determines how sub-shapes are sized and positioned. Valid values: leftright, topdown or border.
noShadow
can be true or false. Set to true if you want to turn of the shadow rendering.
orientation
Applies to decoration shapes (see below) only, to determine where the decoration is positioned within the containing element glyph. Valid values: NW, N, NE, E, SE, S, SW or W.
preferredHeight, preferredWidth
is used in sub-shapes (see next chapter) at N/S (height) or E/W (width) orientation. The defined size is used to calculate the offset for the center shape. You can supply both values in a single shape. In that case only the relevant size will be evaluated.
scaleable
Set to false to prevent rotation of the shape. This attribute is only applicable to the source and target shapes for line glyphs. Valid values: true or false (default = true)

4.4 Sub-Shapes

A so-called sub-shape is a rectangle placed inside10 of shape main. Its purpose is mainly to either place text blocks inside a shape and/or to repeat a certain graphic figure inside a shape. Each sub-shape has a limited width and a certain height. There are two variants of sub-shapes: local and global. Subsequently invoked sub-shapes will be stacked depending on the layoutType property:

topdown
The first sub-shape is placed on top of the main shape. The next one directly below the previous one.
leftright
Similarly but the sub-shapes join to their right edge.
border
when this value is set you can only use a sub-shape invocation with 2 parameters where the first is the name and the second is a compass orientation (N, S, E, W or CENTER). According to the help the shape shall be placed in the according part.

Local sub-shapes have an additional offset you must specify which can be used to compensate the offset and place the sub-shapes arbitrarily inside the main shape11.

A sub-shape is defined by

shape <non-reserved-name> { <block> }
where <non-reserved-name> is any string except main, childelement, label, target, source, LeftTopLabel, MiddleTopLabel, RightTopLabel, LeftBottomLabel, MiddleBottomLabel and RightBottomLabel. <block> consists of any of the previously defined graphic methods like e.g. LineTo, Rectangle, Print and so on. Global ones appears at any top level in a Shape Script like shape main. Local ones are declared inside shape main` at any statement position.

A global sub-shape is invoked inside shape main by calling

AddSubShape(shapeName,width,height)
where shapeName is the <non-reserved-name> of a sub-shape defined in the script. It must be supplied as a string. The width tells how many units of the width for shape main are being used. height specifies the height analogously. Placement of sub-shapes will be according to layoutType as described above.

A local sub-shape has more parameters and is invoked inside shape main by calling

AddSubShape(shapeName,width,height,xOffset,yOffset)
where shapeName is the <non-reserved-name> of a sub-shape defined locally in the script. It must be supplied as a string. The defined shape will be placed inside shape main according to the rules described for the global variant. Additionally, as said, the position will changed by adding xOffset|yOffset.

As you can see the stacking of the sub-shapes is compensated by the -100, -200 and -300 yOffset values. Since the x-coordinate always starts at zero the xOffset is actually always the x-coordinate where the shape starts.

Finally, as mentioned,

AddSubShape(shapeName,compass)
which is required for layoutType = "border".

4.5 Compartments

In the rectangular UML element representation you can find compartments beneath the top compartment showing the name and a few other things. Usually those compartments show attributes and methods. But you can also list anything else inside those compartments. In Shape Script you can create compartments for child elements of an element. For the main element you can have a shape main section. In that case you need to call DrawNativeShape or you will not see any compartments. Now for each child element the Shape Script part shape ChildElement is being processed. Here you can assign a compartment name using SetCompartmentName which will be a grouping criterium. By calling AddCompartmentText you can add the name (and additional information) to the compartment.

shape ChildElement { <block> }
defines the script to be executed for any child element.
SetCompartmentName(name)
will group a subsequent AppendCompartmentText under the specified name. The list of unique compartment names will appear
AppendCompartmentText(text)
will add the supplied text under the according compartment.

To make an example we assume the following element structure where main contains two differently stereotyped elements.

The diagram representation if achieved with the following script12.

1 shape ChildElement { // add Child Compartments to the parent.
2   if(HasProperty("stereotype", "part")) {
3     SetCompartmentName("Parts");
4   }
5   else if(HasProperty("stereotype", "mystereotype")) {
6     SetCompartmentName("My Stereotype");
7   }
8   AppendCompartmentText("#NAMe#"); // case insensitive...
9 }

As you can see the compartments are listed according to the found stereotypes part and mystereotype and they appear in the middle of the rendered element. The names (being print using hash-tags) appear left aligned under each compartment.

4.6 Decoration

If you mainly use the rectangular notation for an element but want to show a fancy icon somewhere you can use

decoration <some-unused-name> { <block> }
where <some-unused-name> is an arbitrary name. It is used at no place but should simply describe the shape in one word. However, the name must be unique amongst the declared decorations. <block> is any graphical description as explained previously.

The default decoration will appear top-left of the shape (NW). If you want it to appear somewhere else you need to assign the orientation property inside to one of the valid compass values.

The following Shape Script

 1 decoration triangle {
 2   orientation = "center";
 3   // Draw a triangle for the decoration 
 4   startpath(); 
 5   moveto(0,30); 
 6   lineto(50,100);
 7   lineto(100,0);
 8   endpath();
 9   setfillcolor(153,204,255);
10   fillandstrokepath();
11 }
12 
13 decoration a {
14   orientation = "NW";
15   setfillcolor(50, 100, 200);
16   rectangle(0,0,100,100);
17   print("test");
18 }
19 decoration b {
20   orientation = "N";
21   setfillcolor(50, 100, 200);
22   rectangle(0,0,100,100);
23 }
24 decoration c {
25   orientation = "NE";
26   setfillcolor(50, 100, 200);
27   rectangle(0,0,100,100);
28 }
29 decoration d {
30   orientation = "E";
31   setfillcolor(50, 100, 200);
32   rectangle(0,0,100,100);
33 }
34 decoration e {
35   orientation = "SE";
36   setfillcolor(50, 100, 200);
37   rectangle(0,0,100,100);
38 }
39 decoration f {
40   orientation = "S";
41   setfillcolor(50, 100, 200);
42   rectangle(0,0,100,100);
43 }
44 decoration g {
45   orientation = "SW";
46   setfillcolor(50, 100, 200);
47   rectangle(0,0,100,100);
48 }
49 decoration h {
50   orientation = "W";
51   setfillcolor(50, 100, 200);
52   rectangle(-50,-50,150,150);
53 }

displays as

You can see that the 100² units are located at fixed positions inside the main shape (the picture shows an enlarged class element). They do not scale with the element. Also if you print text in decorations the font is that being used inside elements in general. There is only one font setting in Enterprise Architect for general use, one for an element in general and one per diagram use. You can not have multiple fonts in one displayed element.

4.7 Labels

It is possible to place labels along with an element oriented at major compass positions. To actually create a label you use

shape label { <block> }
which will contain the instructions to create the label. Usually you will use Print statements and/or graphic methods.
SetOrigin(relativeTo,xOffset,yOffset)
will tell where to actually place the label. relativeTo must be a string constant being one of N, NE, E, SE, S, SW, W, NW or CENTER. The Offset values are measured in screen pixels and can be negative.

4.8 Miscellaneous

Here are a number of methods which will not group elsewhere:

DefSize(width,height)
sets the default size of the shape when it’s initially placed on a diagram or when Alt-Z is used to set the default size. Both parameters define screen pixels. The default for a class element is about 100|80.
DrawParentShape()
is similar to DrawNativeShape. This is used when referring to non-UML shapes defined in MDGs where it will use the shape of the element’s parent from which it is derived. For non-derived elements it will simply behave like DrawNativeShape.
DrawCompositeDiagram()
will enable the rendering of a possibly existing composite diagram.
Image(imageName,left,top,right,bottom)
will place a scaled image inside the units of the shape specified by left, top, right and bottom.

5. Shaping Connectors

Basically the shape for connectors will be defined similarly to that for elements. So most of the previously explained methods can be used for connectors too (of course some – like compartments – do not make sense).

A major difference between both shapes is that elements all have that 100² unit frame. Connectors are not that easy. Shape Script distinguishes between different parts of a connector: source, target, the main connector line and the six labels. For each of them you can define a shape routine:

shape main { <block> }
will define what appears for the connector line.
shape source { <block> }
will define an extra shape at the source end of the connector.
shape target { <block> }
will define an extra shape at the target end of the connector.
shape <labeltype> { <block> }
will draw the <labeltype> labels according to the statements in <block>. Here <labeltype> is one of LeftTopLabel, MiddleTopLabel, RightTopLabel, LeftBottomLabel, MiddleBottomLabel and RightBottomLabel.

Now let’s see what happens when we put some code in these shapes.

5.1 The Connector

We will start with a simple example: using a dash-dotted line which – if my memory does not deceive me – is not used in UML. The following Shape Script is assigned to the base class dependency.

1 shape main {
2   SetLineStyle("dashdot");
3   MoveTo(0,0);
4   LineTo(100,0);
5 }

When used it will produce the following:

That was easy. But what happens when you actually draw something else? Well, let’s try by using the whole 100² units.

1 shape main {
2   MoveTo(0,0);
3   LineTo(100,100);
4 }

It looks like that the pixels below the connector are actually also used to draw something defined in shape main. Going a bit further using this code

1 shape main {
2   Rectangle(0,0,100,100);
3 }

will render as

As you can see the rectangle is rendered on the first section of the connector if there are less than 3 sections. Else it renders on the 2nd section.

5.2 Source and Target

Now that the connector construction is clear let us have a look at the shapes at the endpoints. One of the samples from the Enterprise Architect help will do:

 1 shape main { // draw a dashed line
 2   noshadow=true;
 3   SetLineStyle("DASH");
 4   MoveTo(0,0);
 5   LineTo(100,0);
 6 }
 7 
 8 shape source { // draw a circle at the source end
 9   Ellipse(0,-6,12,6);
10 }
11 
12 shape target { // draw an arrowhead at the target end
13   StartPath();
14   MoveTo(0,0);
15   LineTo(16,6);
16   LineTo(16,-6);
17   EndPath();
18   FillAndStrokePath();
19 }

Here the connector is rendered as dashed line. The source will render a dot and the target a triangle. Finally it will look like this:

Now let us again endeavor a bit more at the endpoints. Let us simply produce a rectangle filling the 100² units and a small circle indicating the 0|0 coordinate:

1 shape source {
2   Rectangle(0,0,100,100);
3   Ellipse(0,-6,12,6);
4 }

and analogously for shape target. Those will render as follows:

As you can see the source shape is rendered below the connector and the target above. I haven’t measured it, but it looks like both occupy 100² pixels. Further you can see that the connector itself will bend even inside the end-shapes.

Actually this means that you need to draw your shapes ±height around the 0-coordinate when you want some symmetrical endpoint. And your endpoint is top left of the shape meaning that you need to draw it reverse (so a possible arrow will point right to left with the tip at 0|0).

5.3 Labels

Simply let us see what happens when we use above methods to render labels:

 1 shape LeftTopLabel {
 2   Rectangle(0,0,100,100);
 3   Ellipse(0,-6,12,6);
 4   print("LeftTopLabel");
 5 }
 6 shape MiddleTopLabel {
 7   Rectangle(0,0,100,100);
 8   Ellipse(0,-6,12,6);
 9   print("MiddleTopLabel");
10 }
11 shape RightTopLabel {
12   Rectangle(0,0,100,100);
13   Ellipse(0,-6,12,6);
14   print("RightTopLabel");
15 }
16 shape LeftBottomLabel {
17   Rectangle(0,0,100,100);
18   Ellipse(0,-6,12,6);
19   print("LeftBottomLabel");
20 }
21 shape MiddleBottomLabel {
22   Rectangle(0,0,100,100);
23   Ellipse(0,-6,12,6);
24   print("MiddleBottomLabel");
25 }
26 shape RightBottomLabel {
27   Rectangle(0,0,100,100);
28   Ellipse(0,-6,12,6);
29   print("RightBottomLabel");
30 }

which renders directly as:

Actually the shapes are rendered as expected and scaled to the size of the containing text. One could expect the left and right top/bottom labels to be a bit more apart (the top being a bit more moved up), but that’s how it is. The top and bottom labels in the middle are placed directly at the same position which looks like a bug13.

5.4 Connector Drawing Methods

There a three drawing methods which are used only in conjunction with connectors.

SetFixedRegion(xStart,yStart,xEnd,yEnd)
defines areas along the connector where symbols should appear.

As you can see there are two regions defined. One for a square in the middle which shows a triangle oriented according to the general rotationdirection (see hash-tags below) of the connector. The second region simply shows a circle.

`HideLabel(labelname)’
will hide the accordingly named label which is a string holding one of the <labeltype> values.
ShowLabel(labelname)
Reveals the hidden label specified by … according to the help. Well.

5.5 Attributes

Analogously to the attributes described in the Shape Attributes chapter here is a list for connectors.

endPointY, endPointX
Only used for the reserved target and source shapes for connectors; this point determines where the main connector line connects to the end shapes.
rotatable
Set to false to prevent rotation of the shape. This attribute is only applicable to the source and target shapes for line glyphs.

6. Properties

As mentioned earlied, Shape Script makes use of a number of properties which can either be used in Print statements enclosed in # (see chapter Strings) or as parameter in the HasProperty operations (see chapter Query Methods).

EA’s documentation of the possible properties leaves some room for improvements - or speculations. So I went through the list and looked for the results found in diagrams when applied.

Basically the properties are strings - and most of them work as expected. While all (well almost) of the strings can be used in a Print statement, their use in HasPropertyis partially restricted to either element or connector shapes.

6.1 Element properties

Trivial

These properties just relate to the ones matching the element properties dialog: alias, author, complexity, datecreated, datemodified, keywords, language, metatype (like defined in a MDG), name, notes, scope, status, stereotype, type (e.g. “Class”)

The two date properties are pointless as you can only test for in-/equality in Shape Script. The format to test is exactly that what you copy from the properties. In my localization it’s “DD.MM.YYYY hh:mm:ss” and I wonder how the guys over the Atlantic ocean will see this to work commonly. I guess it won’t work.

Classifier

If an element (usually Object) has a classifier, its name is available through classifier. Addtionally a couple of (obvious) properties of that classifier are available via classifier.alias, classifier.metatype, classifier.name, classifier.stereotype, classifier.type

Details/Advanced

Properties that appear under the Details tab which deliver either “true” or “false” are isabstract, isactive, isleaf, isroot, isspec, persistence

The context menu Advanced/Multiplicity is reflected under multiplicity.

qualifiedname is what appears for “normal” rendering in the name field of the element. For Object this would include name and classifier.

If rectanglenotation is queried, the context menu Use Rectangle Notation for the diagram object will additionally appear in the Advanced context menu.

This will allow you to draw an alternative shape which can be shown by manually turning off this option.

Miscellaneous

haslinkeddocument
returns “true” or “false” depending on whether a linked document is present.
incomingedge and outgoingedge
return “none”, “left”, “right”, “top”, “bottom”, or “multiple” depending on where one or more in-/outgoing connectors are attached to the shape.
iscomposite
returns “true” or “false” if the element has set a composite structure diagram.
isdrawcompositelinkicon
returns “true” or “false” if the lying 8 should be rendered. This is the case for elements with a composite structure diagram. But not if showcomposeddiagram is “true”.
showcomposeddiagram
returns “true” or “false”. If the element has a composite structure diagram the context for New Child Diagram offers a Show...in Compartment whose state is reflected here.
isembedded and isinparent
return “true” or “false”. Both seem to be the same and return “true” in case an embedded element (e.g. Port or Part) appears. Since those can only appear embedded the query seems pointless. However, I confess that I forgot about embedded rendering. There has been something…
islocked
returns “true” or “false” depending on whether the element has been locked.
istagged
returns “true” or “false” depending on whether the element is bookmarked (see the Advanced Patterns chapter for an example).
isvisible
makes sense for child elements to be processed (see chapter Compartments) depending on their visibility. The main shape is of course always visible.
packagename
usually the name of the package where the element is located.
parentedge
is either “right”, “left”, “top” or “bottom” for Port and Interface depending on which side they are attached to their parent.
parent.metatype
returns the metatype of the parent element or a null string.

Not working

The following either do not work (YAEAB) or I was not able to figure out what they should deliver: cardinality, concurrency, packagepath, partition (though in a print the string appears correctly the query always succeeds), priority, propertytype (and derivatives), stereotypehidden, subtype, visibility

6.2 Connector

Trivial

alias, name and type (e.g. “Association”)
corresponds to the connector’s properties.
effect
value of Effect property for Transition.
guard
value of Guard property for ControlFlow and Transition.
direction
returns one of “Unspecified”, “Bi-Directional”, “Source -> Destination” or “Destination -> Source”.
weight
value of Weight property for ControlFlow.
rotationdirection
returns either “up”, “down”, “left” or “right”. This is calculated from the angle the last part of the connector takes (-45° to +45° will be right etc.).

A number of properties are available for the source and target (<ep> to be replaced) properties of a connector. E.g. the source part of name would be written as source.name.

alias, constraints, multiplicity, name and stereotype
refer to the role properties with the same name like the following.
aggregation
can be “none”, “shared” or “composite”
changeable
“none”, “frozen”, “addOnly”. Located in the Advanced section of the role properties.
multiplicityisordered
“0” or “1” - not “true/false” as the menu suggests. Why be consistent?
qualifiers
semicolon separated list of defined qaulifiers.
targetscope
“instance” or “classifier”. And yes, this is prefixed with target just to confuse the reader. Of course it refers to the Scope role property.
element.name, element.stereotype and metatype
return the properties for the element connected at the specific side. The element’s metatype (trivially corresponding to its type, e.g. “Class”) is (why consistency?) not prefixed with element.

6.3 Diagram

It is possible to query a number of properties of the diagram where the element is currently being rendered. The following work for both element and connector Shape Scripts: diagram.handdrawn returns “0” or “1”, diagram.mdgtype the FQN (e.g. BPMN2.0::Conversation), and also obvious diagram.name, diagram.stereotype, diagram.type (e.g. Logical or Sequence).

Additionally diagram.connectornotation is only available in connector Shape Scripts. It corresponds to the value of diagram properties Connectors/Connector Notation.

7. Advanced Usage

Probably you do not want to define Shape Scripts directly in the stereotypes. More likely you are going to deploy them along with an MDG Technology file. I can not explain how to create MDGs in this book as it would lead much too far. So you either know already how to do that, or you need some outside help.

Another advanced usage is when you simply need more than Shape Script can deliver in respect to querying model contexts. That is where add-ins come into play.

7.1 Shape Script in MDG

Any Shape Script for a profile element must be defined by adding a property named _image and clicking the ellipsis button next to the Initial Value field. This will open the Shape Script editor.

7.2 Add-in

Since Shape Script is so limited in performing algorithms there is an escape through the use of external code hosted in an add-in. If you want to use this feature you need to know how to write add-ins at all. I can not explain how to do that so you need to get outside help for that. But if you know it then here is what you need to take advantage of this escape.

Basically you can retrieve a string value from your add-in which you can evaluate by HasProperty or by directly printing it using a properties. The format is

addin:<addin_name>, <function_name> {, <parameter>}
where <addin_name> is the name you had chosen for your add-in (the identifier) and <function_name> is the name of the function inside your add-in. An arbitrary list of comma separated parameters can be supplied which are passed by value to the called add-in procedure where the repository, the element-GUID and the additional parameters14 are passed. Since Shape Script knows neither variables nor string substitution you need to write those by hand in any case. So a single parameter will usually suffice – or you just have named functions. The called function must return a string as result.

E.g.

1 print("#addin:myAddIn,pFunc1#")

will print the result returned by the function pFunc1 inside your add-in framework.

Similarly

1 hasproperty('addin:myAddIn,pFunc2', '1')

will evaluate to true if your function pFunc2 returns the string value 1.

8. Advanced Patterns

This chapter simply shows a couple of shapes you could adapt for your own use. Currently they are not ordered in any way.

And if the following are not enough you should visit Geert Bellekens’ public repository at https://github.com/GeertBellekens/Enterprise-Architect-Shapescript-Library. It contains all disassembled Shape Scripts that Sparx has used in their MDGs. And, not to forget, Sparx has given permission to make them public.

8.1 Different Actor

This nice actor symbol comes courtesy of Andy J (from the Enterprise Architect forum).

 1 shape main {
 2 // Blue Person courtesy of Andy J;
 3   fixedAspectRatio = "true";
 4   SetFillColor(0,192,255); // light blue
 5   SetPenColor(0,192,255); // ditto
 6   Ellipse(70,-15,90,20);  // head
 7   Rectangle(60,25,100,75); // chest/arms
 8   Rectangle(70,75,90,110); // legs
 9   SetFillColor(255,255,255); // white "shadows"
10   Rectangle(80,75,82,110); // legs
11   Rectangle(66,40,68,75); // left arm
12   Rectangle(92,40,94,75); // right arm
13   Println("#NAME#"); // name it
14   Println("#TAG:Stakeholder Type#"); // add. tag info
15 }

displays as

8.2 Composite Symbol

In case you want to show the lying 8 (composite symbol) on your shape you simply need to add the following at the end of your script:

1 decoration composite {
2   orientation="SE";
3   if(HasProperty("iscomposite","true")) {
4     Ellipse(-80,25,-10,75);
5     Ellipse(10,25,80,75);
6     MoveTo(-10,50);
7     LineTo(10,50);
8   }
9 }

8.3 Non-/Rectangular Notation

If you want the user to allow switching between rectangular and iconic representation you simply can do that with

1 if (HasProperty('rectanglenotation', '0')) {
2   // code for iconic representation
3 ) else {
4   // code for rectangular representation
5   // e.g. Rectangle or DrawNativeShape
6 }

If the shape script finds this query it will show the Advanced/Use Rectangular Notation in the context menu.

8.4 Rotating According to Incoming Edge

Unfortunately you can not use a sub-shape and have something rotated. You need to draw each rotation individually. However, the following pattern will enable you to do that:

1 if(HasProperty("incomingedge","bottom")) {
2   // draw view with connector at bottom
3 else if(HasProperty("incomingedge","top")) {
4   // draw view with connector at top
5 else if(HasProperty("incomingedge","left")) {
6   // draw view with connector at left
7 else  {
8   // draw view with connector at right
9 }

8.5 Cloud Compensation

When using a cloud path it is not predictable how much the puffs will extend outside the frame. However, it is possible to get it into the bounding frame to some extend by adding padding shapes:

 1 layouttype="border";
 2 addsubshape("padding","n");
 3 addsubshape("padding","s");
 4 addsubshape("padding","e");
 5 addsubshape("padding","w");
 6 addsubshape("cloud","center");
 7 
 8 shape padding {
 9   preferredwidth=10;
10   preferredheight=8;
11 }
12 
13 shape cloud {
14   startcloudpath();
15   rectangle(0,0,100,100);
16   endpath();
17   fillandstrokepath();
18   // add name shape and other text
19 }

8.6 Remove Connector Stereotype

If you define a stereotyped connector the stereotype itself will be displayed in the middle. If you don’t want it to appear add the following to your connector Shape Script:

1 shape middlebottomlabel { }

Of course there is another way leading to Rome:

1 HideLabel("middlebottomlabel");

inside shape main will have the same effect.

8.7 Highlighting

There was a time when almost every week a post about an obscure little red triangle was found at Sparx’ Enterprise Architect forum. This feature to highlight elements seems to be a bit underestimated. When you want to highlight elements you can do so by Shift-Space. This will also set the tagged property. By adding the following in your Shape Scripts you can render a red triangle which is a bit larger than the original one.

1 decoration tagged {
2   orientation = "N";
3   if(HasProperty("istagged","true")) {
4     SetFillColor(255,0,0);
5     Polygon(50,-50,3,60,30);
6   }
7 }

And here is how it looks like:

8.8 Envelope

A commonly used shape for message objects is an envelope:

1 shape main {
2   fixedAspectRatio = "true";
3   DefSize(40,25);
4   Rectangle(0,0,100,100);
5   MoveTo(0,0);
6   LineTo(50,50);
7   LineTo(100,0);
8 }

8.9 Annotation

Quite some profiles use a non-closed rectangle for annotations. The following draws the open side on the right except when the incoming note link is attached to the right. In that case the open side is on the left.

 1 shape main {
 2   noshadow = true;
 3   layouttype="border";
 4  
 5   if(HasProperty("incomingedge","right")) {
 6     MoveTo(50,0);
 7     LineTo(100,0);
 8     LineTo(100,100);
 9     LineTo(50,100);
10   } else {
11     MoveTo(50,0);
12     LineTo(0,0);
13     LineTo(0,100);
14     LineTo(50,100);
15   }
16  
17   AddSubShape("padding","W");
18   AddSubShape("padding","N");
19   AddSubShape("padding","S");
20   AddSubShape("padding","E");
21   AddSubShape("line","CENTER");
22 
23   shape padding {
24     preferredwidth=10;
25     preferredheight = 10;
26   }
27  
28   shape line {
29     editablefield = "note"; // does not work
30     PrintWrapped("#NOTES#");
31   }
32 }

The above envelope and an annotation looks like this:

8.10 Arrows

The following – a bit lengthy – example shows how to create large arrows. Using tagged values the orientation, fill and name display can be changed.

 1 shape main {
 2   noShadow=true;
 3   if (HasTag('orientation', 'up')) {
 4     DefSize(90,70);
 5     StartPath();
 6     MoveTo(50,0);
 7     LineTo(100,25);
 8     LineTo(75,25);
 9     LineTo(75,100);
10     LineTo(25,100);
11     LineTo(25,25);
12     LineTo(0,25);
13     LineTo(50,0);
14     EndPath();
15   } else if (HasTag('orientation', 'down')) {
16     DefSize(90,70);
17     StartPath();
18     MoveTo(50,100);
19     LineTo(0,75);
20     LineTo(25,75);
21     LineTo(25,0);
22     LineTo(75,0);
23     LineTo(75,75);
24     LineTo(100,75);
25     LineTo(50,100);
26     EndPath();
27   } else if (HasTag('orientation', 'left')) {
28     DefSize(70,90);
29     StartPath();
30     MoveTo(0,50);
31     LineTo(25,100);
32     LineTo(25,75);
33     LineTo(100,75);
34     LineTo(100,25);
35     LineTo(25,25);
36     LineTo(25,0);
37     LineTo(0,50);
38     EndPath();
39   } else { // default right
40     DefSize(70,90);
41     StartPath();
42     MoveTo(100,50);
43     LineTo(75,100);
44     LineTo(75,75);
45     LineTo(0,75);
46     LineTo(0,25);
47     LineTo(75,25);
48     LineTo(75,0);
49     LineTo(100,50);
50     EndPath();
51   }
52   if (HasTag("fill","true"))  FillAndStrokePath();
53   else StrokePath();
54 }
55 
56 shape label
57 {
58   SetOrigin("SW",0,0);
59   if(HasTag("nameVisible","true")) Print("#NAME#");
60 }

This could look like the following:

8.11 Lines

Sometimes you want to place a simple line on a diagram to make some kind of separation. Here’s a neat trick how to do that. First we define a connector shape based on a dependency:

1 shape main {
2   noShadow = "true";
3   SetPenColor(GetUserBorderColor());
4   SetPenWidth(10);
5   MoveTo(0,0);
6   LineTo(100,0);
7 }
8 
9 shape middleBottomLabel {}

When you connect two elements with this dependency you will just see a thick line.

Now the tricky part: Since a connector needs to be connected to something just create a shape

1 shape main {} // draw nothing!

which will actually draw nothing. Now place two boundaries on the diagram and connect them with the connector defined above. Finally you need to assign the invisible shape to the boundaries. Now you will only see the line itself.

The following screen shot shows an arbitrary shaped line with one invisible boundary selected:

8.12 Perfect Circles

Nothing is perfect, but you can approach perfection. If you need circles in your shape which will not distort to ellipses when scaling you need to do it like this:

1 shape main {
2   fixedAspectRatio = "true";
3   DefSize(100,100);
4   Ellipse(0,0,100,100);
5 }

The fixedAspectRatio will scale X and Y always in the same ratio. The DefSize needs two identical pixel sizes to form a square. Inside that all circles will remain circles.

9. Shape Script Syntax

This is the EBNF for Enterprise Architect’s Shape Script language. The start symbol is ShapeScript. Any spaces and tabs between non-terminals are ignored.

ShapeScript = { Shape | Decoration };

Shape = “shape”15 ShapeName ShapeBody;

ShapeName = /* any reserved or non-reserved string literal depending on the context */;

Decoration = “decoration” Name ShapeBody;

Name = /* an arbitrary string that should describe the form of the decoration */;

ShapeBody = “{“ {InitializationAttributeAssignment} {DrawingStatement} {SubShape} “}”

SubShape = Shape /* with a non-reserved name */;

InitializationAttributeAssignment = Attribute “=” Value “;”;

Attribute = /* see chapter Shape Attributes for a list of values /*;

Value = StringLiteral | Integer | Tuple;

StringLiteral = Quote { Character } Quote;

Quote = /* the double quote “ or a single quote ‘ */;

Character = /* any printable character except the used Quote */;

Integer = [”-“] {“0” .. “9”};

Tuple = “(“ Integer “,” Integer “)”;

Block = “{“ {DrawingStatement} “}” | DrawingStatement;

DrawingStatement = IfElseSection | Method;

IfElseSection = “if” “(“ QueryExpression “)” Block [“else” Block];

QueryExpression = /* see chapter Query Methods for the 2 methods and their parameters */;

Method = /* see chapter Shaping Elements for possible methods and their parameters */;

Notes

1The EA version used to create this book was actually 10.0 (build 1009). However, most of the references are also valid for earlier versions of EA. And of course all this still works with version 13.5 when the latest edit was made on this book. Though I have not cross checked which bugs were fixed until 13.5. Honestly, V14 itself was so buggy I never used it. I might cross check with V15.2 but I’m not to keen installing newer EA versions.

2I really loathe writing such legal blurb since it should be obvious. By the way: German Law applies! (Does that change anything?)

3This name will appear on top of the existing list of stereotypes so you can edit your test cases faster than with one named test. I use ae as prefix for element stereotypes and ac for connector shapes in this book. You are asked to use meaningful names for you stereotypes instead.

4For a real case you should limit it it the base element where you want to appear the stereotype.

5Once a script is assigned the button will appear as Edit and the Preview below will contain what it says.

6The Next Shape button will only be relevant if there are more than one shapes defined in a script. The preview will simply loop through the shapes and display one after the other.

7I highlighted the top left and bottom right coordinates in the screen shot.

8Not so say “the most advanced feature in Shape Script”.

9I have no idea whether it is planned to extend the Shape Script language. Here it looks like there should be a possibility to work with objects in the future. The current implementation does not look very meaningful else.

10Actually you can place it also outside the 100² units by using coordinates above 100 or below 0.

11This is one of the moments where I think that language designers which are responsible for that should be clubbed to death with a teaspoon. Preferably using one made of plastic so it takes longer.

12The script already uses control structures being explained later in this book. But hopefully it’s already understandably here.

13Feel free to report a bug.

14Since I use Perl, I only can see the repository and the GUID parameter so I have to use unique functions. This is perfect anyhow as these add-in methods should be used in rare cases only.

15Actually you can arbitrarily replace “shape” with “label” and “text”. You may do so to confuse others.