GDL Handbook A COMPREHENSIVE GUIDE TO CREATING POWERFUL ARCHICAD OBJECTS 2nd Edition
Andrew Watson
PUBLISHED BY Cadim...
954 downloads
3616 Views
6MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
GDL Handbook A COMPREHENSIVE GUIDE TO CREATING POWERFUL ARCHICAD OBJECTS 2nd Edition
Andrew Watson
PUBLISHED BY Cadimage Group Ltd. 41 Taharoto Road Takapuna Auckland, New Zealand www.cadimageworld.com
Copyright © 2011 Cadimage Group Ltd. All rights reserved. No part of the contents of this book may be reproduced or transmitted without the written permission of the publisher.
National Library of New Zealand Cataloguing-in-Publication Data Watson, Andrew, 1970GDL handbook : a comprehensive guide to creating powerful ArchiCAD objects / Andrew Watson. 2nd ed. Previous ed.: 2009. Includes index. ISBN 978-0-473-18838-2 (pbk.)—978-0473-18839-9 (ebook) 1. ArchiCAD (Computer file). 2. Architectural design—Data processing. 3. Computer-aided design—Handbooks, manuals, etc. I. Title. 729.0285—dc 22
Cover Design: Campbell Yule
ArchiCAD® and GDL are registered trademarks of GRAPHISOFT®, Virtual Building™ is a trademark of GRAPHISOFT®.
i TABLE OF CONTENTS 1
2
3
4
Introduction .................................................................................................................. 1 1.1
GDL.................................................................................................................................. 1
1.2
What can be Modeled using GDL? ........................................................................... 2
1.3
Cadimage...................................................................................................................... 3
1.4
Resources ....................................................................................................................... 4
1.5
How to Use this Book..................................................................................................... 4
Anatomy of a GDL Object ........................................................................................ 6 2.1
What is a GDL Object? ................................................................................................ 6
2.2
Creating an Object ...................................................................................................... 8
2.3
Open an Existing Object for Editing ........................................................................... 9
2.4
Placeable Objects ...................................................................................................... 10
2.5
Object Subtype ........................................................................................................... 11
2.6
Parameters ................................................................................................................... 12
2.7
The Parameters List ..................................................................................................... 13
2.8
Subtypes, Parameters & Template Objects ............................................................ 16
2.9
Object Details .............................................................................................................. 18
2.10
The GDL Scripts ............................................................................................................ 19
2.11
Preview Window .......................................................................................................... 23
2.12
Drawing Fragments .................................................................................................... 23
2.13
2D Symbol .................................................................................................................... 24
2.14
2D Full View .................................................................................................................. 25
2.15
3D View......................................................................................................................... 25
2.16
Preview Picture ............................................................................................................ 25
Variables ..................................................................................................................... 26 3.1
Assigning a Value to a Variable ............................................................................... 26
3.2
Naming Variables ....................................................................................................... 28
3.3
Variable Type............................................................................................................... 30
3.4
Array and Vector Variables....................................................................................... 32
3.5
Scope............................................................................................................................ 36
Operators and Expressions ...................................................................................... 38 4.1
The Equals Operator ................................................................................................... 38
ii
5
6
7
8
9
4.2
The Inequality Operator............................................................................................. 40
4.3
Brackets and the Order of Operations.................................................................... 41
4.4
Logical Operators ....................................................................................................... 42
4.5
Bit Operators ................................................................................................................ 45
Working with Text Strings .......................................................................................... 49 5.1
Number of Characters in a String ............................................................................ 49
5.2
Search for a Text within a String................................................................................ 50
5.3
Read Part of a String .................................................................................................. 50
5.4
Substitute Text .............................................................................................................. 50
5.5
Display Numeric Data as a String ............................................................................. 51
5.6
Display the Current Date and Time as a String ...................................................... 55
5.7
Extract Numeric Data from a String ......................................................................... 57
5.8
Include a Quotation Mark in a String Constant ..................................................... 58
Using the Stack .......................................................................................................... 59 6.1
Put, Use and Get Values ............................................................................................ 59
6.2
Use ‘Put’ and ‘Get’ to Simplify Scripts ..................................................................... 60
6.3
When to Use the Stack ............................................................................................... 62
6.4
Clearing the Stack ...................................................................................................... 62
6.5
Put and Get Data within a Block ............................................................................. 63
Loops........................................................................................................................... 64 7.1
The ‘for–next’ Loop ..................................................................................................... 64
7.2
The ‘repeat – until’ and ‘while – endwhile’ Loops................................................. 67
7.3
Infinite Loops ................................................................................................................ 68
7.4
Practically Infinite Loops ............................................................................................ 72
Scripting Style ............................................................................................................ 76 8.1
Readable Code .......................................................................................................... 76
8.2
Structured Scripting .................................................................................................... 82
8.3
Managing Transformations........................................................................................ 84
8.4
Legible vs. Efficient ..................................................................................................... 84
Macros and Sub-Routines ....................................................................................... 86 9.1
Macros .......................................................................................................................... 86
9.2
Sub-Routines ................................................................................................................ 96
iii
10
11
12
13
14
9.3
Re-Usable Sub-Routines ............................................................................................. 98
9.4
Which to Use? ............................................................................................................ 103
The 2D Symbol ......................................................................................................... 107 10.1
2D Transformations .................................................................................................... 107
10.2
Attributes .................................................................................................................... 110
10.3
Quick-and-Dirty Ways to Produce a 2D Symbol .................................................. 111
10.4
Parametric 2D Elements ........................................................................................... 116
10.5
Drawindex .................................................................................................................. 133
The 3D Model ........................................................................................................... 136 11.1
General Controls ....................................................................................................... 136
11.2
3D Transformations .................................................................................................... 139
11.3
Basic 3D Elements ..................................................................................................... 140
11.4
3D Polygons and Planes........................................................................................... 145
11.5
Prisms, Slabs and Extrusions...................................................................................... 147
11.6
Revolved Forms ......................................................................................................... 154
11.7
Tubes ........................................................................................................................... 157
Groups and Solid Element Operations ................................................................ 161 12.1
Solid Element Operations ........................................................................................ 161
12.2
Defining a Group of Elements................................................................................. 163
12.3
Placing a Group ........................................................................................................ 164
12.4
Nested Groups .......................................................................................................... 164
12.5
Group Operations ..................................................................................................... 164
Text ............................................................................................................................. 167 13.1
Selecting a Font ........................................................................................................ 167
13.2
Define a Text Style..................................................................................................... 168
13.3
Text Size....................................................................................................................... 170
13.4
Place a Single Line of Text ....................................................................................... 171
13.5
Define a Paragraph.................................................................................................. 172
13.6
Using Paragraphs ...................................................................................................... 176
13.7
Text Orientation ......................................................................................................... 178
13.8
Text in 3D .................................................................................................................... 181
Working with Graphics ........................................................................................... 183
iv
15
16
17
14.1
File Types .................................................................................................................... 183
14.2
Create an Image with Transparency .................................................................... 183
14.3
Images for the Object Settings Dialog .................................................................. 184
User Interface .......................................................................................................... 188 15.1
Selection and Referencing ..................................................................................... 188
15.2
Graphic Editing ......................................................................................................... 191
15.3
Customizing the Settings Dialog ............................................................................. 198
15.4
Images in the Settings Dialog ................................................................................. 201
15.5
Outfields ..................................................................................................................... 202
15.6
Basic Input Fields ....................................................................................................... 203
15.7
Values Lists & Graphic Infields................................................................................. 205
15.8
Buttons ........................................................................................................................ 212
15.9
Tool Tips....................................................................................................................... 214
15.10
Organizing Data........................................................................................................ 215
15.11
Interactive Parameters ............................................................................................ 217
Approximating Curves ........................................................................................... 220 16.1
Circular Arcs .............................................................................................................. 220
16.2
Ellipses ......................................................................................................................... 223
16.3
Helices ........................................................................................................................ 224
16.4
Bezier Splines.............................................................................................................. 226
16.5
Traditional Cubic Splines.......................................................................................... 240
Linear Algebra 101 ................................................................................................. 253 17.1
Scalars and Vectors ................................................................................................. 253
17.2
Vector Components ................................................................................................ 255
17.3
Vector Notation ........................................................................................................ 255
17.4
Vector Diagrams ....................................................................................................... 256
17.5
Length of a Vector ................................................................................................... 256
17.6
Equivalent Vectors.................................................................................................... 257
17.7
Adding Vectors ......................................................................................................... 257
17.8
Scalar Multiplication ................................................................................................. 259
17.9
Unit and Zero Vectors............................................................................................... 260
17.10
Cross Product ............................................................................................................ 261
17.11
Perpendicular Vector in 2D ..................................................................................... 264
v
18
19
20
17.12
Dot Product................................................................................................................ 265
17.13
Equation of a Line ..................................................................................................... 266
17.14
Equation of a Plane .................................................................................................. 267
Linear Algebra Applications ................................................................................. 268 18.1
Does a Point Lie on a Line Segment? .................................................................... 268
18.2
A Test for Parallel Lines ............................................................................................. 273
18.3
Intersection of Two Lines in 2D ................................................................................ 276
18.4
Intersect 2 Line Segments in 2D .............................................................................. 279
18.5
Intersect a Line Segment and an Arc in 2D .......................................................... 280
18.6
Calculate an Angle .................................................................................................. 287
18.7
Intersection of Two Arcs ........................................................................................... 289
18.8
Equation of a 2D Arc through Three Points ........................................................... 292
18.9
Closest Point on a Line ............................................................................................. 298
18.10
Closest Point on a Line Segment ............................................................................ 302
18.11
Closest Point on a Polygon ...................................................................................... 305
18.12
Distance between a Point and a Line Segment ................................................. 305
18.13
Distance between Two Lines .................................................................................. 306
18.14
Find the Points of Closest Approach ...................................................................... 310
18.15
Distance between a Point and a Plane................................................................ 314
18.16
Closest Point on a Plane .......................................................................................... 314
18.17
Intersection of a Line and a Plane ......................................................................... 315
Matrices and Transformations ............................................................................... 316 19.1
Elementary Transformation Matrices ..................................................................... 316
19.2
Matrix Multiplication ................................................................................................. 318
19.3
Combined Transformations ..................................................................................... 320
19.4
Translations ................................................................................................................. 321
19.5
The xform Command ............................................................................................... 321
19.6
Calculating Natural Co-ordinates .......................................................................... 323
Freeform 3D Modeling............................................................................................ 324 20.1
GDL Primitives ............................................................................................................ 324
20.2
Cardboard Cut-Outs ................................................................................................ 328
20.3
Basic Texture Mapping............................................................................................. 337
20.4
3D Modeling .............................................................................................................. 340
vi
21
22
23
20.5
Freeform Modeling ................................................................................................... 347
20.6
Advanced Texture Mapping .................................................................................. 357
20.7
Randomizing Objects ............................................................................................... 361
20.8
Trouble Shooting ....................................................................................................... 363
Polygon Operations ............................................................................................... 364 21.1
Defining a Polygon ................................................................................................... 364
21.2
Polygon Area ............................................................................................................. 365
21.3
Ensure a Positive Polygon Sense ............................................................................. 368
21.4
Check if a Point Lies Inside a Polygon ................................................................... 369
21.5
Polygon Addition ...................................................................................................... 372
21.6
Intersection of Two Polygons................................................................................... 381
21.7
Subtract One Polygon from Another ..................................................................... 383
21.8
Offset a Polygon ....................................................................................................... 385
21.9
Closest Point on a Polygon...................................................................................... 390
21.10
Center of Gravity ...................................................................................................... 391
21.11
Optimizing the Polygon Operations....................................................................... 394
Quality Control ........................................................................................................ 396 22.1
Scripting Style ............................................................................................................ 396
22.2
Testing ......................................................................................................................... 400
22.3
Fixing Issues ................................................................................................................ 402
22.4
Optimising for Speed................................................................................................ 403
22.5
Reduce the Polygon Count .................................................................................... 408
22.6
Provide Optional Data ............................................................................................. 413
22.7
Use Group Operations Sparingly ............................................................................ 413
Development Strategies........................................................................................ 414 23.1
A Model for Development ...................................................................................... 415
23.2
Opportunity ............................................................................................................... 418
23.3
Some Thoughts on Planning and Design .............................................................. 421
23.4
Improving the Resource Pool.................................................................................. 424
23.5
Implementation ........................................................................................................ 426
23.6
Testing ......................................................................................................................... 427
23.7
Maintenance............................................................................................................. 427
vii 24
Example Objects ..................................................................................................... 428 24.1
Playground Equipment/Barrel Bridge .................................................................... 428
24.2
Playground Equipment/Swing ................................................................................ 431
24.3
Wine Glass .................................................................................................................. 450
ix
Acknowledgements The GDL Handbook reflects on some of what I have learned over several years of GDL programming. Many people have contributed to my GDL education over this time. Some names in particular come to mind.
Theo de Klerk without whom my GDL programming career would never have started.
Campbell Yule, Tracey Gatland and the team at Cadimage for providing a secure and challenging programming role, and for taking the risk of publishing this book.
Debbie Garrett, Michael Posthuma and Johanna Bilton for their help with proof-reading.
Glen Richardson for many valuable discussions on GDL programming.
The help given by each of you has been much appreciated. Thank you. Finally I would like to acknowledge the involvement of all ArchiCAD users who have become a part of the Cadimage community over the years. You guys have provided the challenges and at times criticism necessary to shape and test the validity of my ideas. I trust that the GDL Handbook will in turn help other GDL programmers, both as a learning resource and as a daily reference.
xi
Foreword to the Second Edition Since publishing the first edition of the GDL Handbook back in 2009, things have moved on. New GDL commands have been introduced, some old work-arounds have been rendered obsolete, and my own programming skills have matured. However most of the content of the first edition remains valid. The reason for this longevity is that the GDL Handbook was always designed to stand the test of time, by focussing on core aspects of GDL programming, namely program structure and design, problem-solving, geometry, and standard solutions to common problems. In this second edition, much of the original content has remained intact, with minor adjustments as necessary to reflect some of the changes that have taken place in the past three versions of ArchiCAD. I have taken the opportunity to re-format the page layout to make it easier to see at a glance which chapter and section you are currently reading. All images now include captions that serve a dual role as references within the main text, and as supplementary comments. I’ve also tried to make the text generally more readable, smoothing out some of the rougher and more cryptic passages, and using a larger type. Some of the original sample scripts included typographical errors. In most cases these were either trivial (e.g. an error in a comment of the script) or else clearly wrong. Wherever such errors have been detected, they have been corrected in this edition. The section in on Smarter Font Selection originally included in chapter 13 Text has been removed, as it relied on being able to read and write data to the ArchiCAD Data Folder. This can still be done, but since the advent of Windows 7 writing to files in the Programs folder has become problematic for the majority of end-users. Please note three particularly important changes to the second edition. The first major change is the method of responding to button click events and parameter changes in the Parameters script. In the first edition, I recommended clearing the values of glob_modpar_name and glob_UI_button_ID at the foot of the script. This approach is no longer possible, due to a change introduced in ArchiCAD 13. A new solution has been provided by Graphisoft, and this is given on page 198. The second change is a quite serious bug that I found in the sub-routine for finding the intersection of two line segments. This bug has been corrected on pages 281 - 282. And last but not least, many of you will be pleased to finally have this electronic version of the book.
1
Introduction
ArchiCAD is a computer aided design (CAD) package developed by Graphisoft, a Hungarian software business, for use by architects. Since its initial release in the mid 1980s it has carved a niche for itself as the preferred architectural design and documentation solution. The basic premise of ArchiCAD is that the bulk of an architect’s work should be carried out at the design stage, and that the benefits of this initial work should flow through the subsequent documentation. ArchiCAD uses the concept of a virtual building or building information model (BIM) as the design medium. The architect effectively constructs a model of the building structure on the computer. Having created a 3D model, the task of producing construction documentation is fast-tracked. Plans, sections and elevations can be generated by creating different views of the model. As a result, any changes to the model are automatically reflected in the drawings. What remains is to attach necessary details, notes and dimensions before publishing the documentation. To create a virtual building you need virtual tools. The tools that ArchiCAD provides are building elements and objects. A selection of 2D elements (lines, fill polygons, arcs, text and labels) is provided for drawing annotation. The building elements include fundamental structures such as slabs, walls, columns, beams and roofs. Pretty much everything else is modeled using programmable objects. ArchiCAD objects, then, are used to model a vast range of physical entities, including items as diverse as medical equipment, office furniture, trees, vehicles, playground equipment, kitchen cabinets, plumbing fittings, people, doors, windows and stairs. As you can see, objects are not restricted to components of the building, but can also be used to provide content and environment for visualizations.
1.1 GDL GDL (Geometric Description Language) is a niche programming language used to create objects and libraries (collections of objects) for use within ArchiCAD. Because objects are generally stored in libraries, they are often referred to as library parts. Objects can be used to place 2D symbols into projects, to create 3D models and to output components and quantities 1. Compared to many programming languages, GDL is easy to learn, especially if you have a good appreciation of space and form. Programming in GDL can also be very rewarding. With a relatively few lines of code, you can create impressive 3D
1
Outputting components and quantities is beyond the scope of this book. Eric Wilk’s book ArchiCAD: From CAD to Quantity Survey covers the subject rather comprehensively.
2 Chapter 1: Introduction - What can be Modeled using GDL? structures. It’s true that GDL has its fair share of quirks, frustrations and limitations. As time goes by, the language evolves and gradually the problems are ironed out. I have been programming in GDL for over a decade now, and have seen the introduction of a user interface palette, array variables, dynamic hotspots, group operations and many other improvements to the language. These improvements have become so much a core of the current tool-set that it is now difficult to imagine working without them. One of the aims of this book is to give you the inside running on how to use the building blocks well, avoiding the pitfalls. From that point you will begin to use your skills and other resources to push the boundaries of GDL.
1.2 What can be Modeled using GDL? This book is largely about using GDL to draw 2D symbols and model 3D structures. 3D modeling is the most creative aspect of GDL. Many of the resources outlined in the following pages are designed to help model objects ranging from very simple forms created using the most basic GDL building blocks, to highly parametric library parts and freeform bodies. To give you an idea of how the various resources can be used, consider the following objects.
Figure 1 – A child’s swing modeled using GDL
The chain links of this playground swing (figure 1) were modeled using the tube command discussed in chapter 11 The 3D Model. They also draw on chapter 7 Loops and chapter 22 Quality Control. The seat was modeled from primitive elements as explained in chapter 20 Freeform 3D Modeling, using the curve approximation methods of chapter 16 Approximating Curves.
The flowers of the daylily shown in figure 2 use the cardboard cutout technique described in chapter 20 Freeform 3D Modeling, and images with transparency as explained in chapter 14 Figure 2 – Natural forms Working with Graphics. The leaves are modeled using the primitive elements of chapter 20. can also be modelled
Chapter 1: Introduction - Cadimage 3 The branch system of this tree (figure 3) is constructed using primitive elements as explained in chapter 20 Freeform 3D Modeling. It draws heavily on techniques described in chapters 16 Approximating Curves and chapters 17 & 18 on Linear Algebra. The tree can be randomized as discussed in chapter 20. If this option is selected, every point on its surface is re-calculated. The techniques of chapter 22 Quality Control are used to ensure the extra processing does not cause undue delays.
Figure 3 – Randomized tree modeled in GDL.
This New Zealand cabbage tree (figure 4) was created using the cardboard cutout technique described in chapter 20 Freeform 3D Modeling, and images with transparency as explained in chapter 14 Working with Graphics. The image used comes from Envirographic’s Trees of the South Pacific collection. Figure 4 – GDL Cabbage Tree
The children’s adventure playground shown in figure 5 includes a number of objects. All of the objects (platforms, slides, bridges, rails, etc.) are created with regular GDL bodies such as prism, tube and extrude discussed in chapter 11 The 3D Model. The cargo net (in the background), slide and wobbly bridge use techniques discussed in chapter 16 Approximating Curves. Mapping textures to surfaces is discussed in chapter 20 Freeform Modeling. All of the above objects use techniques covered in the earlier chapters. For more Figure 5 – GDL Playground
architectural objects such as doors, windows, stairs, cabinets and the like, algorithms such as those given in chapter 21 Polygon Operations are essential.
1.3 Cadimage Cadimage (the company that pays my salary) produces third-party add-ons for ArchiCAD. These tools enrich the virtual building concept by adding extra information, enhancing design flexibility, managing the documentation process and extending the application of ArchiCAD. Among other tools, Cadimage produces a Keynotes system for managing
4 Chapter 1: Introduction - Resources annotation, a Revisions tool for managing the flow of documentation, design tools such as Cadimage Stairs and Cadimage Doors+Windows, and extension packages such as the Cadimage Planting and Cadimage Wall Frames. For more information on these tools, visit our website at http://www.cadimageworld.com. All of the Cadimage tools use GDL library parts. Some are very complex, others relatively simple. Many of the tools also use API add-ons (.gdx and .apx files) written in C++. Creating add-ons is outside my scope of expertise, but they can add extra intelligence to library parts. The Cadimage Stair is one example of how API and GDL programming can work together to produce a powerful tool. Each flight of stairs, and each section of railing, is a separate GDL object. The objects are linked via the add-on to interact as a complete assembly. I have been involved to some degree in the design and creation of each of these tools. Some of them date back over a decade at the time of writing, others are just months old. A comparison of the older and newer objects would reveal how my understanding of GDL and scripting style have developed over the years. My current ideas on design and implementation have been formulated and improved on the basis of countless mistakes and (I trust) more successes. This book is intended to eliminate the trial-and-error approach that characterized my own learning, and to fast-track the reader’s development by providing definitive solutions to common problems.
1.4 Resources GDL programming draws on a wide range of resources. Among other things, these resources include the programming language itself, solutions to common mathematical and geometrical problems, and re-usable macros and sub-routines. Quality resources are vital to a programmer, just as sharp tools are important to a carpenter. This book sets out to provide a working toolbox of resources that have been collected and developed over years of GDL programming. These resources have proved consistently useful for a large number of projects including such varied applications as wall framing, trees, doors & windows, cabinets and 2D elements for detailing. Each development project you undertake will require a different selection of resources. You will find yourself using some of them time and time again. Others will be required only on the odd occasion. But all are valuable when it comes time to use them. Over time you will extend and refine your collection of resources, as those provided in the following pages are generally useful but by no means form a complete set.
1.5 How to Use this Book If you are new to GDL, don’t get bogged down in the details. You can do a lot of good programming at an introductory level. Just skim over the text so that you are aware of the basic concepts, and are aware of any pitfalls that may exist. As you begin to use the various resources in practical situations, you will gain a better appreciation for the details and may find it worth re-reading a section here and there. If you are already familiar with GDL, use this book as a supplementary resource, or simply as a stimulus for thought.
Chapter 1: Introduction - How to Use this Book 5 While you will no doubt already be using many of the algorithms and techniques discussed in this book, you will appreciate the luxury of having a central resource bank. You may not agree with all of my comments and suggestions but I think you will find enough valuable content to make it a worthwhile read. Either way, I would suggest that you read the whole book rather quickly at first, to get a feel for its content. If you find something of immediate interest, make a note of it so you can come back to it later, or use the index to reference topics as required. Because even a relatively simple object uses techniques discussed in several chapters, it is not possible to provide meaningful examples of specific techniques. Instead I have included a few examples of complete scripts in the final chapter. Feel free to re-create these objects if you like. The exercise may be worthwhile if it gives you a feel for how GDL objects can be put together.
2
Anatomy of a GDL Object
As a GDL developer, most of your work will be carried out within the object editing environment. Before getting involved with the syntax of GDL, then, I’d like to take some time to introduce you to the editing environment. Think of yourself as a GDL medical student. Before you start performing surgery on a GDL object, take an introductory course in GDL anatomy. If you understand how the object is put together, and which bits go where, you will be better equipped to avoid gruesome mistakes 2. In this chapter I’ll show you how to open the editing environment, how it is structured, and how to navigate to the various parts. We will also discuss the object’s data structure and some of the basic concepts that underpin GDL scripting. You’ll see how all the parts of the object fit together, and how each part contributes to the whole object. In the following chapters we’ll discuss how each part can be made to do useful work. We’ll also take a brief look at special objects. These objects have all the same bits and pieces as regular objects, but perform slightly different tasks. Some simply work behind the scenes, and are never used directly by ArchiCAD designers. Others are invested with special powers either by ArchiCAD itself (doors and windows, for example, stay attached to walls, and automatically cut holes for themselves) or by add-on software (Wall Coverings remain glued to walls). But essentially all objects are the same and should be treated with equal respect. Once you have worked through this chapter, you’ll have a broad understanding of how the various parts of a GDL object fit together. You’ll be able to open any object up, examine its inner workings, and (assuming it has been well designed) have a reasonable idea about what it is intended to do in different contexts.
2.1 What is a GDL Object? As an ArchiCAD user, you know how to select a GDL object from a library, edit the settings, and place it into the floor plan, section, detail or worksheet in which you are working. You will be familiar with the Object, Stair, Door, Window and Label tools that appear in the ArchiCAD Toolbox, and the Coverings tools that are accessed via the Cadimage menu. All of these tools are library parts. Due to force of habit (perhaps a bad one) I use the terms object and library part interchangeably. Once placed, a library part’s settings can be adjusted graphically, or by re-opening its Settings dialog. In the project plan view, the object will appear as a set of lines, arcs, fill polygons, text and other 2D elements. Typically these 2D elements
2
Despite a programmer’s best intentions, such mistakes will unfortunately arise from time to time. But with a little debugging, most objects can be saved.
Chapter 2: Anatomy of a GDL Object - What is a GDL Object? 7 are arranged to represent a diagram of the selected object as seen from above. In the 3D window, the object may appear as a 3D form – a virtual model. In a Section or Elevation window, the object may show up as an outline or shaded view of the virtual model, including cut cross-sections wherever the section line passes through the object. As a GDL programmer, you will begin to think of an object (i.e. a library part) as of an integrated set of parameters, preferences, fragments and scripts.
2.1.1 Parameters The term parameter refers to data that describes some aspect of an object. For example, in a collection of objects designed to model playground equipment, the Slide object might include a numeric parameter that describes the height of the slide. In other words, the user can enter numeric data that determines the slide’s height. Another parameter controls the slide’s width. The user interacts with the object by adjusting these parameters to control various aspects of the object’s structure and appearance.
Figure 6 – Parameters of a slide object presented as fields in a Settings dialog.
An object’s parameters may be presented to the user as fields in the object’s Settings dialog (figure 6). In this case, the parameter values can be adjusted by typing directly into the fields. The fields may be presented as direct input (type a number or some text into a box), or a selection from a list of values. Alternatively, parameter values may be adjusted graphically using dynamic hotspots in the Floor Plan, 3D, Section or Elevation views (figure 7). In either case, the effect is to change or edit the object. Parameters are not restricted to linear dimensions of the object. They can also include angular measures, pen, material, line type and fill indices, text strings, integers and real numbers. Figure 7 – Parameters can be adjusted graphically in 3D.
2.1.2 Preferences Preferences are global settings for the object. These control such characteristics as whether it is a regular object, a window or a door, and whether hotspots should be automatically placed at the corners of the bounding box in plan view.
8 Chapter 2: Anatomy of a GDL Object - Creating an Object
2.1.3 Fragments A series of 2D drawings (lines, arcs and fill polygons) can be stored with each object. These drawings are stored in the drawing fragments and can be used as part of the object’s 2D symbol.
2.1.4 Scripts Each of the object’s scripts is a list of instructions that tells the GDL interpreter how the object should behave in various contexts, and how the parameters affect it. For instance, one script is a set of instructions that is followed whenever the object is displayed in the ArchiCAD plan view window. Another script instructs the GDL interpreter how to Figure 8 – Create a new object. create the 3D model. A third script presents the user with a set of fields and instructions on opening the object’s Settings dialog.
2.2 Creating an Object Before you can start any GDL development, you should know how to create a new object. To create a new object: 1. Choose Libraries and Objects > New Object from ArchiCAD’s File menu (figure 8). A new window will open that looks something like this (figure 9). This is the object’s editing window. Note that this has changed somewhat in ArchiCAD 15, with new migration scripts added. Figure 9 – The object’s editing window.
2. Choose Save from the ArchiCAD File menu (figure 10).
Figure 10 – Save the new object.
Chapter 2: Anatomy of a GDL Object - Open an Existing Object for Editing 9 3. In the File Save dialog, use the Go to Library button to select one of the loaded libraries, and then navigate to the correct sub-folder within the library (figure 11). 4. Type a name for the object in the File name field. The name must be unique (no two objects in the loaded libraries may share the same name). 5. Finally, click on the Save button to save the new object. Try it Yourself: Create a new Object For practice, follow the steps above to create a new object, named My Slide in a new folder called Playground Equipment then close the GDL Figure 11 – Use the Go to Library button to choose editing window. one of the loaded libraries, then click on Save. A Word of Caution In the above example, we created a My Slide object in brand-new library folder called Playground Equipment. Usually, when you save an object, you will save it into an existing library. If this is the case, be extremely careful to save it into the correct library. It is surprisingly easy to save a new object into the wrong folder. I still do it occasionally, and it can be extremely frustrating later on when you are trying to find where you saved it. It is especially easy to make this mistake when you are working with multiple versions of the same library (i.e. one version that works in ArchiCAD 13, another updated for ArchiCAD 14, etc.). To avoid this mistake, always Figure 12 – Open an object for editing use the arrow button labeled Go to Library to navigate to one of the currently loaded libraries. While it’s still possible to load the wrong library, the risk is reduced.
2.3 Open an Existing Object for Editing Our new My Slide object from the preceding section contains no parameters or GDL instructions, and therefore does nothing. We must add parameters and GDL scripts before we have an adjustable 3D model and 2D symbol for our slide. To edit an object you must open its GDL editing window: 1.
Choose Libraries and Objects > Open Object from the File menu as
Figure 13 – Browse to select the object.
10 Chapter 2: Anatomy of a GDL Object - Placeable Objects shown in figure 12. 2.
In the Open Library Item dialog, browse for the object you want to edit, and choose Open (figure 13).
3.
The object’s GDL editing window will appear.
Each time you want to edit an object, you will have to open its editing window. Other Ways to Open a Library Part for Editing There are other ways to open an object’s editing window. For these alternative methods, you don’t have to browse in the file dialog to find the object. The object will be automatically selected from the loaded library. If an instance of the object has been placed into the project, you can select it and open the editing window directly. To open an object’s editing window from a placed instance: 1.
Select an instance of the object you want to edit.
2.
Choose Libraries and Objects > Open Object from ArchiCAD’s File menu (figure 12).
If you are editing an object in which there is a call to another object, you can select the name of the second object from the text of the GDL script, and open the second object directly. To open an object’s GDL editing window from text: 1.
Highlight the name of the object in the GDL script.
2.
Choose Libraries and Objects > Open Object from the File menu (figure 12).
Note that you can also highlight an object name in the Session Report window to directly open its editing window.
2.4 Placeable Objects The term placeable is given to an object that is available for selection in the library browser, and can thus be placed into an ArchiCAD project. Objects that are not placeable are not available in the library browser, and therefore cannot be selected by the user for placement into the project. By default, all new objects are placeable. Any GDL object may be defined to be either placeable or non-placeable. To define whether an object is placeable or non-placeable:
Figure 14 – The Placeable checkbox is located at the top right of the editing window.
Chapter 2: Anatomy of a GDL Object - Object Subtype 11 1.
Open the object’s GDL editing window.
2.
Click on the Parameters button in the menu on the left.
3.
Click on the Placeable checkbox to change the status (figure 14) and Save the change.
If an object was originally placeable, and was placed into an ArchiCAD project, it will remain in the project even if its status is later changed to non-placeable. This can be useful if an object becomes obsolete. It can be left in the library, so that existing projects remain intact, while being removed from the library browser so as to prevent its being used in new projects. Non-placeable Objects for use with Add-ons In some cases, an add-on may be used to place an object. This is true of the Wall Covering add-on, for instance. It places a Wall Covering object and gives it data about the wall to which it is linked. It would make no sense for a user to select the object from the ArchiCAD toolbar and place it directly into the project, as it would then not be linked to any wall. 3 You may want to prevent users from selecting and placing an add-on controlled object manually. In that case, simply make the object non-placeable. The add-on will still be able to place the object. Non-Placeable Macros Later we’ll take a look at macros. These are library parts that are not directly placed into a project. Rather they are used as resources by another object. Typically you do not want the end user to see these macros. Once again, the solution is to make the macros non-placeable. In this way, only the main objects will appear in the library browser.
2.5 Object Subtype Throughout this book, I will refer to library parts interchangeably as objects. This can be a bit confusing. The ArchiCAD toolbox clearly distinguishes between Objects, Doors, Windows, Stairs and so on (figure 15), whereas we will refer to all of these as different library part (or object) subtypes. Some library part subtypes possess unique characteristics. Figure 15 – Sairs, doors and
For instance, an object whose subtype is window can only be placed in a wall, and will windows are listed separately in automatically cut an opening into the wall. the toolbox.
3
Actually this is not strictly true – sometimes it can make a lot of sense to place wall coverings into the project manually. However let’s not get too fussy over the details.
12 Chapter 2: Anatomy of a GDL Object - Parameters An object whose subtype is Label may be accessed via the Label tool and through special tab pages in the settings dialogs of other objects (figure 16). It turns out that there are many more subtypes than there are tools in the Toolbox. Only a few of the object subtypes (e.g. doors, windows, labels) are associated with separate tools. Most are lumped together under the generic Object tool. When you choose a subtype for an object you created, you are defining some basic information about the object’s characteristics. It’s important that you choose an appropriate subtype for your object.
Figure 16 – Library parts with the subtype ‘label’ may be accessed from the Listing and In most cases, selecting a subtype adds a bunch of standard parameters to the Labelling section of the Settings Dialog.
object. At least in some cases (e.g. stairs, doors, windows) the subtype also adds default IFC category and parameters for use with IFC. 4 To select a subtype for an object: 1.
Open the object’s editing window.
2.
Click on the Select Subtype … button. A subtype browser will appear.
3.
Choose the best option for the subtype.
4.
Click the Select button to select the subtype.
2.6 Parameters GDL objects are parametric. This means that certain characteristics of an object can be controlled by the end user. The user might adjust a pen color, change a material, or set a dimension to modify the shape or appearance of Figure 17 – Choose a subtype for each library the object. A GDL object used to model a playground Barrel Bridge may include 4
part you create.
IFC is a file interchange format that allows different CAD applications to share data. You can create a virtual building in ArchiCAD, and export it to an IFC model. An engineering firm using a different CAD package can then import the BIM model and produce relevant documentation. The IFC classification for an object placed into an ArchiCAD project can be selected from the list in the Listing and Labelling tab.
Chapter 2: Anatomy of a GDL Object - The Parameters List 13 parameters to control: •
The bridge length or span.
•
The barrel diameter.
•
The width of the wooden slats.
•
The thickness of the wooden slats.
•
The width of the steel bands used to hold the slats together.
•
The thickness of the steel bands.
•
The thickness of the supporting beams.
•
The depth of the supporting beams.
•
The material of the timber components.
•
The material of the steel bands.
Figure 18 – A barrel bridge requires a large number of parameters to completely describe it.
Each of these characteristics is described by a parameter of the object. A parameter is a set of data that includes a variable, a name, a type and a default value. For example, a parameter of type length might be named Board Width, with a variable boardWidth and a default value of 90mm. Any number of parameters can be added to an object to give all the necessary controls. The more parameters an object has, the more control the user has over its details. On the other hand, excessive numbers of parameters can result in an overly complex object. One of the challenges of designing an object is to strike a balance between flexibility and simplicity of use.
2.7 The Parameters List The Parameters section of the object’s editing environment lists all of the parameters that describe and control the object.
Figure 19 – Edit the parameter list of a library part. Click on a parameter in the list to select it, then edit the name, the variable name, format the parameter (i.e. bold, indented or hidden) or set it’s default value.
14 Chapter 2: Anatomy of a GDL Object - The Parameters List To select one of the parameters in the list, click on the variable or name of that parameter (figure 19). You can add a new parameter by clicking on the New button at the Figure 20 – Change the parameter’s type. top of the palette. To delete a parameter, click to select the parameter in the list and click the Delete button at the top of the palette. To change the name, variable, or default value of a parameter, click to select the parameter in the list, and type directly into the fields. To change the parameter type, click on the type icon (figure 20). A selection palette will appear. Click on one of the options to select a Figure 21 – In normal use, formatted parameters can different parameter type. 5 appear as bold or indented.
2.7.1 Formatting the Parameters List You can format the appearance of the Parameters list (figure 21). You can arrange the parameters in logical groups, hide parameters Figure 22 – Bold-face parameter. that you don’t want users to change, and highlight important parameters. The formatting is applied in normal use (i.e. the enduser is adjusting settings), not in the object editing environment. To display a parameter name in bold-face type, click on the B button (figure 22). To hide a parameter, select the parameter and click to activate the X button (figure 23). The parameter will be hidden when the object is in normal use.
Figure 23 – Hide a parameter.
To change the order of listed parameters, click on the Slide Arrows to the left of the parameter (figure 24), and drag the parameter up Figure 24 – Move a parameter up or down the list. or down. To indent a parameter when in normal use, click on the Indent Figure 25 – Indent a parameter. 5
We’ll come back to the concept of parameter type later, when we discuss variables. For now the important point is that this is where you choose the parameter type.
Chapter 2: Anatomy of a GDL Object - The Parameters List 15 button (figure 25). Note that only one level of indents is available. In many cases, multiple parameters are required to describe a single characteristic of an object. For instance, to describe the frame of a window, you may require parameters to set the frame width, thickness, material, pen, and so on. In such a case it can be convenient to group these parameters together in the list. To do so, create a title-type parameter and indent the associated parameters directly below it. When using the object, the indented parameters will be tucked neatly out of sight. To view the parameters, the user must click on a button beside the title.
2.7.2 Unique Parameters It is possible to define a parameter to be unique. Click on the U button (figure 26) to define a parameter as unique. The term ‘unique’ in this case means that if the settings of one instance of an object are transferred to another instance of the object in the library browser window, the parameters marked as unique will not be transferred. This can be very useful. Say you have created an object to model a roof truss. When you transfer parameters from one roof truss to another, you may not want to transfer the roof pitch across. This is unique to the placed instance of the roof truss object. You do want to transfer the dimensions of the truss members (top chord, bottom chord, web, etc.). By making the roof pitch parameter unique, you can guarantee that this value will not be transferred. Ideally this functionality could be extended to cases where parameters are transferred between placed instances of objects, or from favorite settings.
Figure 26 – Set the U flag to make the parameter unique.
2.7.3 Common Parameters Four special parameters are “almost always” included when you create a new object. These have the variables A, B, ZZYZX and AC_show2DHotspotsIn3D. Bounding Box Hotspots The first three of these parameters are intended to define the bounding box of the main part of the object. The width is set by A, the depth by B, and the height by ZZYZX. The A and B parameters have a special significance, as these are used by the object when
Figure 27 – Bounding-box hotspots are added to most objects.
16 Chapter 2: Anatomy of a GDL Object - Subtypes, Parameters & Template Objects placing it into a view. When you place an object into the plan view, you get to choose one of four placement options. Two of these (the ‘diagonal’ and ‘rotated diagonal’ geometry methods) involve a stretch to indicate the object size. When you use one of these methods to place an object, you are graphically adjusting the A and B values. You can also graphically edit the bounding box of an object at any time after it has been placed (figure 28). Stretching the bounding box has the effect of adjusting the A, B or ZZYZX values. Show 2D Hotspots in 3D The ac_show2DHotspotsIn3D parameter is a Boolean (checkbox) Figure 28 – Bounding-box hotspots can be used to type parameter. By default its value is set to 1 (On). Its purpose is to graphically adjust the A, B and ZZYZX parameters. replicate the 2D hotspots in the 3D model. I can’t recall any example of this parameter being at all useful. I generally set its value to 0 (Off) before saving a new object.
2.8 Subtypes, Parameters & Template Objects When you selected a subtype for the My Slide object, a number of parameters were automatically added to the list. This always happens when you choose a subtype. We say that the object inherits the parameters of the subtype. In other words, if a parameter of the subtype does not already exist in the object, it is automatically added. All parts of the parameter are added, including the name, variable, type, formatting and default value. If a parameter having the same type and variable already exists in the object, it remains unchanged. If a parameter having the same variable but a different type already exists in the object, it is removed and replaced by the subtype’s parameter. You are not limited to choosing a subtype from the defaul list, as you can create your own subtypes. To create a new subtype: 1.
Create a new object (or save a copy of an existing object)
2.
Choose a subtype for the object.
3.
Add parameters that you want to be automatically inserted into all objects of the new subtype.
4.
Set the Template checkbox (Boolean) parameter to 1 (On).
Chapter 2: Anatomy of a GDL Object - Subtypes, Parameters & Template Objects 17 5.
Save the object.
Try it Yourself: Playground Equipment Template Eventually, our Playground Equipment library may include a collection of slides, swings, platforms, tubular steel rails, bridges, chains, cargo nets and more. Before we start to add new library parts to the collection, we should create a special template (subtype) object named Playground Equipment. For one thing, this will logically group the objects. If a user chooses to browse by subtype in the library browser, all the Playground Equipment objects will appear together. Having a Playground Equipment subtype will also facilitate more structured development. All the parameters of the template object will automatically be added to any new object when we assign it the subtype Playground Equipment. A number of parameters will be common to many of the Playground Equipment objects. For instance, we will be using a lot of timber in Figure 29 – The Playground Equipment template our playground construction. All the timber components should includes all common parameters for this collection. have the same control for material, so our subtype should include a parameter named Timber Material with variable timberMat and default value 15 (the material with index 15 is Pine in most ArchiCAD project templates). By including this parameter in our template object, it will be automatically included in each new Playground Equipment type object we create. Other common parameters will be included in the template to control post width, board width, thickness & spacing, rope material and thickness, cable material and thickness, and so on. So let’s go ahead and make a new Playground Equipment subtype. Create a new object, set its Subtype to Model Element > Site Improvement, and save it as Playground Equipment into your Figure 30 – The Playground Equipment template is now Playground Equipment library. listed with the other subtypes.
1.
Click on the Template checkbox to set its value to 1 (On).
18 Chapter 2: Anatomy of a GDL Object - Object Details 2.
Click on the Placeable checkbox to set its value to 0 (Off).
3.
Add parameters to the object as illustrated (figure 29).
4.
Finally Save the object.
When you browse the list of Subtypes, you will now see that a new subtype called Playground Equipment has been added to the list (figure 30). Open the My Slide object, and change its Subtype to Playground Equipment. You’ll see that it inherits all of the parameters from the Playground Equipment object (figure 31). Tips •
Notice that all parameters belonging to the sub-type are displayed in blue text. These parameters cannot be removed from the list, although their order, name and default values may be changed. If you choose a different subtype, the parameters inherited from the original Figure 31 – Change the subtype of you’re My Slide object to be Playground Equipment. subtype will be stripped from the list.
•
As a sort of quick and dirty work-around, you can use a sub-type to add a bunch of parameters to an object. If you then close the ArchiCAD project and remove the sub-type from the library, the object will display MISSING SUBTYPE. At that point, all of the parameters will remain in the object, but can now be edited as you like. You can then choose a different subtype without losing the parameters.
•
If you later edit the parameter set belonging to the sub-type, the changes will be inherited by all objects that use that sub-type.
2.9 Object Details When you create a new object, the Details section provides some extra control over the object’s behavior. Click on the Details button to bring up the Details palette. The content of this palette will differ depending on the object’s subtype.
2.9.1 Regular Objects For regular objects, the Details palette lists three options (figure 32). Always choose the Save Fixed Values option and
Chapter 2: Anatomy of a GDL Object - The GDL Scripts 19 leave the two checkboxes at their default settings (i.e. both set to Off). In ArchiCAD 14, there are also some settings for migrating libraries across versions of ArchiCAD. These settings really only exist for historical reasons. In the old days, things were done a bit differently. Graphisoft has provided these settings to allow library parts created under the new and old regimes to co-exist.
2.9.2 Doors and Windows
Figure 32 – The Details palette for objects.
Doors and windows have a different set of preferences (figure 33). As with regular objects, you can generally leave these at their default values, but there are some special settings you need to consider. • You can type a single-line GDL expression into the Derived Sizes fields. • The Nominal Frame Thickness must be set correctly, or the door/window will not flip correctly. • Use the Graphisoft oversize parameters (ac_left_oversize, ac_right_oversize, etc.) for the Wall Opening Oversizing fields. You can set the values of the parameters in the Parameters Script.
2.10 The GDL Scripts
Figure 33 – The Details palette for Doors and Windows.
Each object includes a set of GDL scripts. Each script is a list of instructions for the GDL interpreter. When the scripts run, they are converted into instructions to perform useful tasks such as drawing 2D symbols, building 3D models, adjusting parameter values, or outputting quantities. In other words, the scripts make the objects work. Without them, the objects would be lifeless. There would be no 3D model. A simple, lifeless 2D symbol is the best you could hope for. In this section we’ll take a quick overview of the various scripts that make up a GDL object. In the following chapters we’ll consider each script in greater detail.
2.10.1
Master Script
At run-time, the Master Script is included at the start of each of the other scripts. When you run the 3D Script to create a model, the Master Script runs before the 3D Script. Similarly, it is run before the 2D Script when you create a 2D symbol. Try it Yourself: See how the Master Script is Added to the Head of Other Scripts To see how this works, make a new object, and type the following code into the Master Script editing window.
20 Chapter 2: Anatomy of a GDL Object - The GDL Scripts !Report Master Script Running print “Now running the Master Script …”
Now type the following code into the 2D Script editing window. !Report 2D Script Running print “Now running the 2D Script …” !Draw Something rect2 0, 0, a, b
Copy the following code into the 3D Script editing window. !Report 3D Script Running print “Now running the 3D script …” !Draw Something brick a, b, zzyzx
Click on the 2D Full View button to run the 2D Script. You will see two alert messages, one informing you that the Master Script is running, the next that the 2D Script is running. Now click on the 3D View button to run the 3D Script. You will see that the Master Script also runs before the 3D Script. Because the Master Script runs before the other scripts, you can use it to perform any common calculations that you need. The Master Script is also a good place to declare any constants that will be used by more than one of the main scripts. One Use of the Master Script When you compare the values of two linear dimensions to see if they are practically equal, you cannot use the ‘=’ equals operator. Instead you must check whether the difference between the two numbers is within an acceptable tolerance. Because such comparisons are used frequently, I always set up a variable tol in the Master Script that I can then access in any other script. Usually I set the value of tol to a fraction of a millimeter. !Define a Tolerance tol = 0.00001
We’ll return to this concept when we consider mathematical operations, so if it doesn’t mean much right now don’t worry – all will be explained later.
2.10.2
2D Script
Click on the 2D Script button to open the 2D Script editing window. The 2D Script can be used to create a 2D symbol. For 3D objects, the symbol usually represents the plan view of the object. This may be identical to an axonometric projection of the 3D model as viewed from above. Often, though, it is a symbolic representation, and may include a label to identify what the symbol represents.
Chapter 2: Anatomy of a GDL Object - The GDL Scripts 21 Depending on what subtype is selected, the 2D symbol can also be used for 2D ‘patch’ objects (useful for detailing), for labels or for door/window markers. Try it Yourself: A 2D Script for the My Slide Object Open the My Slide object from the Playground Equipment library for editing, and add length-type parameters runIn and runOut, with default values of perhaps 1 metre (or 3 feet). Open the 2D Script editing window, and add the following GDL script. !Basic Slide Shape !Draw the slide polygon poly2_b 4, 7, gs_fill_pen, gs_back_pen, 0, 0, 1, runIn + a + runOut, 0, 1, runIn + a + runOut, b, 1, 0, b, 1 !Contour Lines to show the Edges line2 0, edgeThick, runIn + a + runOut, edgeThick line2 0, b - edgeThick, runIn + a + runOut, b – edgeThick
Figure 34 – Click on the 2D Full View button to run the 2D script.
!Contour Lines to show the Changes in Slope line2 runIn, 0, runIn, b line2 runIn + a, 0, runIn + a, b
Click on the 2D Full View button to pre-view the slide’s 2D symbol. If you copied the 2D Script correctly, and set up the parameters correctly in the Parameters list (including the default values), you will see something like this (figure 34).
2.10.3
3D Script
Click on the 3D Script button to open the 3D Script editing window. The 3D script is used to define a 3D model for the object. Various commands are used to create vertices, edges, polygons and bodies. ExampleExercise: A 3D Script for the My Slide Object Open the My Slide object for editing, and type the following script into the 3D Script editing window. !Create the Slide resol 18 tube 8, 6, 51, !Profile
22 Chapter 2: Anatomy of a GDL Object - The GDL Scripts 0, 0, 0, b, 0, 0, b, edgeHeight, 0, b - edgeThick, edgeHeight, 0, b - edgeThick, edgeThick, 0, edgeThick, edgeThick, 0, edgeThick, edgeHeight, 0, 0, edgeHeight, 0, !Path -1, 0, zzyzx, 0, 0, 0, zzyzx, 0, runIn, 0, zzyzx, 0, runIn + a, 0, 0, 0, runIn + a + runOut, 0, 0, 0, runIn + a + runOut + 1, 0, 0, 0
Figure 35 – Click the 3D button to run the 3D script and view the result.
Click on the 3D View button to run the script. The 3D Preview window will open. Provided you copied the script, typed the parameter variables and entered the default values correctly in the Parameters list, the 3D Preview should look something like that shown in figure 35. Admittedly, it’s not the sort of slide that I would like to use personally. Those sharp edges could do a lot of damage, and I think there would be quite a bump as you hit the end. But hey, it’s a start.
2.10.4
Parameter Script
Click on the Parameter Script button to open the Parameter Script editing window. The Parameter Script is where you define values lists for parameters. You can also lock (disable) parameters, and hide them if they Figure 36 – The interface script is used to design appear in the Parameters list. One very important function of the custom Settings dialogs. Parameter Script is that you can use it to automatically set parameter values. This is extremely useful for error handling and dynamic user interaction, as we will see later. For now, our My Slide object is so basic that we don’t need to add anything to its Parameter Script.
Chapter 2: Anatomy of a GDL Object - Preview Window 23
2.10.5
Interface Script
Click on the Interface Script button to open the Interface Script editing window. This script is used to design Settings dialogs to help users enter data more intuitively. While the Parameters list works OK for objects that contain only a few parameters, and where the function of each parameter can be easily described in one or two words, it is difficult to use for objects that contain large numbers (possibly hundreds) of parameters. It’s also difficult for users to work with the Parameters list when array parameters are used, when values lists contain non-intuitive values, or when parameters require better description. The Interface Script allows you to arrange fields, text, buttons, tool tips and images to provide an intuitive Settings dialog for Figure 37 – The preview window displays the result of running the 2D or 3D scripts. your object.
2.10.6
Property Script
Click on the Property Script button to open the Property Script editing window. This script runs when lists are compiled. Its main use is to output quantities and descriptions. The topic of outputting quantities is not discussed in this book. There are other books available that discuss the intricacies of quantity output and calculation.
2.11 Preview Window At the top left of the object’s GDL editing palette (figure 37) is a Preview Window. You can use this while editing your object to get a preview of the work you have done. The Preview Window provides a very small picture of the object.
Figure 38 – The 16 Drawing Fragment buttons are used to show or hide the different fragments in the It is normally more useful to click on the 3D View or 2D Full View preview window. These buttons have been removed buttons, as these provide a much larger picture in which you can navigate in ArchiCAD 15.
to view all the detail.
2.12 Drawing Fragments Directly below the Preview Window you’ll see 16 buttons (figure 38).
24 Chapter 2: Anatomy of a GDL Object - 2D Symbol Each of these buttons controls the visibility of a Drawing Fragment. Drawing Fragments are like layers within the 2D symbol of an object. Later we’ll see that you can draw a 2D symbol directly into the object. The various elements you use to draw the symbol are each assigned to one of the Drawing Fragments, just as elements you use in ArchiCAD are assigned to a layer. By pressing one of the 16 buttons down, you make the corresponding Drawing Fragment visible when you open the 2D Symbol window. The drawing fragments seem to promise an attractive alternative to Figure 39 – The 2D Symbol window. genuine GDL scripting. You can draw lines, arcs and fill polygons without any knowledge of the GDL language. However, you will find this approach extremely limited and increasingly frustrating. In practice drawing fragments are more trouble than they’re worth.
2.13 2D Symbol The 2D Symbol button opens a window in which you can draw a static 2D symbol for the object (figure 39). While this is generally not the best way to create a 2D symbol, you should be aware of the option. It can perhaps be useful when you are working with objects that have little or no parametric control but relatively complex shapes.
Figure 40 –2D Full View window.
For example, if you modeled a car in a freeform modeling package, you might use the 2D Symbol window to draw a static plan view representation. (A better approach would be to drag-and-drop the lines, arcs and fill polygons directly into the 2D Script editing window.) Click on the 2D Symbol button to open the 2D Symbol drawing window of the object. You can draw 2D elements such as lines, hotspots, arcs, circles and splines into this window. These will be displayed as a part of the object’s 2D symbol if: • There is no 2D Script, in which case the 2D Symbol fragments will be used instead. • The 2D Script contains a fragment2 statement. This is an instruction for the GDL interpreter to use one of the drawing fragments of the 2D Symbol window as part of the 2D representation.
Chapter 2: Anatomy of a GDL Object - 2D Full View 25 If neither of these two cases applies, the elements you drew in the 2D Symbol will not form a part of the actual 2D symbol displayed by the object.
2.14 2D Full View The 2D Full View button opens a preview window that shows the object’s 2D symbol as it will be displayed in the project plan view (figure 40). This differs from the 2D Symbol window, as that window shows only the drawing elements associated Figure 41 – The 3D View window. with certain fragments, and does not display any scripted drawing elements.
2.15 3D View The 3D View button opens a preview window that shows the object’s 3D model representation (figure 41).
2.16 Preview Picture Click on the Preview Picture button to open the object’s Preview Picture window Figure 42 – The Preview Picture (figure 42). You can paste an image into this window. The image will be displayed window. as an icon for the object in the library browser. The Preview Picture is 128 x 128 pixels in size. To get the best results, the picture you paste into the Preview Picture window should match these dimensions. When designing a Preview Picture, you should avoid fine vertical or horizontal lines, as these may disappear when the icon is displayed at a smaller size. Use simple icons with filled areas, or photo-rendered images, rather than outlines. Try it Yourself: Create a Preview Picture Use Adobe Photoshop to draw a Preview Picture for the My Slide object (figure 43). Figure 43 – Use areas of colour to design Copy it from the Photoshop window and paste it into the object’s Preview Picture a preview picture. window.
3
Variables
A GDL script is simply a list of instructions that is performed when the script runs. Some of the instructions may create 3D structures such as spheres, blocks or tubes. Others may create 2D structures such as arcs and lines. Many of the instructions in a GDL script will include some type of calculation. When the script includes an instruction to draw a line, for example, it will need to specify the end points. These will likely be calculated based on the current parameter values. It is of course possible that the line end points are constant values, but this would allow for very little adjustment of the object. Where the result of a calculation will be used repeatedly throughout a GDL script, it is often convenient to store the resulting value at a known location in the computer’s memory. This would be true in the case of a steel reinforcing mesh. The user will set the rod diameter via a parameter value. However the radius is what will be used whenever a rod is modeled. To calculate the radius is somewhat trivial, we simply divide the diameter by two. Rather than constantly recalculating this value, it is convenient to perform the calculation once and store the result. To store the result of a calculation, we assign it to a variable. A variable is an identifying name for a location in the computer’s memory. It can be any text string (with some limitations as discussed later). When we use a variable as part of an expression in a GDL script, the value of the data found at the referenced memory location will be substituted at run-time. When a variable is set to be the result of a calculation, the calculated value is stored at the memory location assigned to that variable. In practice, you can think of a GDL variable in much the same way as an algebraic variable. In this chapter, we will consider how variables can be used in GDL scripts, and how operators and expressions can be used to adjust their values.
3.1 Assigning a Value to a Variable To assign a value to a variable, use the equals operator ‘=’. Type the variable name, followed by the equals operator, then type the value you want to assign. The value may be given as a constant or as an expression.
3.1.1 Assigning a Constant Value to a Variable To assign a constant value to a variable: 1.
Type the variable.
2.
Type an equals ‘=’ operator.
Chapter 3: Variables - Assigning a Value to a Variable 27 3.
Type the constant value you want to assign to the variable.
Try it Yourself: Directly Assign a Value to a Variable Create a new object, and type the following GDL code into the 2D Script editing window. !Set the value of variable seatHeight to 900mm seatHeight = 0.900 print seatHeight
The first line of the GDL code is an instruction to store the value 0.900 in the variable seatHeight (a reference to a location in memory). The next line is an instruction to print the value of variable seatHeight to an alert message. Click on the 2D Full View button to run the script. An alert message will come up displaying the number 0.9.
3.1.2 Assign the Value of One Variable to Another Variable You can assign the value of one variable to another variable. To do so: 1.
Type the variable.
2.
Type an equals ‘=’ operator.
3.
Type the variable whose value you want to assign to the new variable.
Try it Yourself: Assign the Value of One Variable to Another Variable Create a new object, and type the following GDL code into the 2D Script editing window. !Set the value of variable seatHeight to match that of variable ZZYZX seatHeight = zzyzx print seatHeight
The first line of the GDL code is an instruction to store the current value of the variable zzyzx in the variable seatHeight. You may recall that the variable zzyzx belongs to a parameter that is added to all objects (well, nearly all) GDL objects. Variables belonging to parameters already have values assigned to them and can be used directly in the GDL scripts.
3.1.3 Assign the Result of an Expression to a Variable You can assign the result of an algebraic expression to a variable. When you do so, the expression will first be evaluated, and its result then assigned to the variable. To assign the result of an expression to a variable:
28 Chapter 3: Variables - Naming Variables 1.
Type the variable.
2.
Type an equals ‘=’ operator.
3.
Type the expression whose calculated value you want to assign to the variable.
Try it Yourself: Assign the Result of an Expression to a Variable To see how this works, create a new object and type the following GDL code into the 2D Script editing window. !Initialize variables seatHeight and seatThick seatHeight = zzyzx seatThick = 0.040 !Initialize variable legLength as the result of an expression legLength = seatHeight – seatThick !Report the value of legLength print legLength
The first line of the GDL code is an instruction to store the value 0.900 in the variable seatHeight. The second line of code is an instruction to store the value 0.040 in the variable seatThick. The third line of code is an instruction to calculate the value of the expression (seatHeight – seatThick), and to store the value in the variable legLength. The next line is an instruction to print the value of variable legLength to an alert message. Click on the 2D Full View button to run the script. An alert message will come up displaying the number 0.86.
3.2 Naming Variables There are some restrictions on variable names. In particular, the first character of a variable cannot be a digit. Also there are certain characters (such as : , * / + - % { and so on) that can’t be included, as they are used for specific GDL commands. Generally it’s a good idea to stick to alphabetical and numeric characters. However, avoiding the use of illegal characters when naming a variable is like avoiding swear words when naming a baby. Choosing an appropriate name goes much further than this. The approach you take to naming variables is extremely important. Well-chosen variable names can make your GDL script readable, while poorly chosen names can make for a very confusing script. The name you assign to a variable should clearly describe the dimension or quantity that it represents. For example, in the Barrel Bridge object we considered in chapter 2 Anatomy of a GDL Object, I would probably use the variable boardWidth to hold the width of the boards. We could use any other name for the variable, for instance BW or X, but somehow boardWidth is easier to understand. When you see the variable boardWidth in your script, you don’t have to wonder “Now then, does that stand for board thickness, or perhaps the space between the boards?” No, the variable clearly identifies the quantity it is designed to represent.
Chapter 3: Variables - Naming Variables 29 Until you have had the misfortune to work on a script where variables have been poorly named, you may not appreciate how important it is to choose variable names well. I have wasted many hours re-naming variables to make scripts readable. Many of those scripts I had written myself as a novice programmer. At one time I had the misguided idea that it was better to use abbreviations rather than complete words, as it was quicker to type. I have regretted this bright idea ever since, whenever such a script requires maintenance. A word of caution is needed here. You should take care to limit the length of your variables to be no longer than 30 characters. Variable names that are longer than this can result in some odd and elusive issues, as I experienced while working on Cadimage’s Wall Framing tool. I used the variable studSetoutRestartAfterJunctions for a Boolean parameter that controlled how the framing interacted with intersecting walls. It worked perfectly well until I placed the parameter as a checkbox field into the Settings dialog. When I clicked on the checkbox the dialog dropped all of its fields to become a flat gray panel. When I shortened the variable name to studsRestartAfterJunctions, everything behaved correctly. Often a complete object is composed of multiple parts. In the case of our wall framing example, the frame object is composed of a set of framing members (top plate, bottom plate, studs, etc.). Each member is described by a number of variables, for example memberWidth[], memberThickness[], memberLength[], memberPosition[][], memberOrientation[], memberType[]. GDL is not an object-oriented language, but you can still identify all the variables that belong to a given structure by naming them in a consistent way. I find it good practice to use a common prefix (e.g. member…) to identify variables belonging to a particular structure (in this case a framing member). Similarly a Barrel Bridge object might have one set of parameters prefixed barrel to control the shape of the barrel, another set prefixed board that control the boards used to construct the barrel, and a third set prefixed beam that control the supporting beams. Counter variables used in loops are often named i, j or k, or are prefixed with one of these characters. Whenever you see such a variable your first assumption should be that it is a counter variable. Counter variables can also be useful when keeping track of transformations (add, mul, rot, xform, etc.). Dynamic hotspots must be given a unique index, so I use an integer variable iHotspot as a counter for this purpose. When we look at primitive elements, we will see that counters can be used to keep track of the vertices, edges and polygons. The prefix n is used to indicate the total number of items. Thus a variable nBoards might be used to store the total number of boards used by the Barrel Bridge object. One other common variable type is a geometric vector. Variables u, v and w are generally reserved for geometric vectors. These may be given as a 3-valued array u[], v[], w[], or as single-valued component variables ux, uy, uz, vx, vy, vz, wx, wy, wz. When naming array variables, I used to prefix the variable with an ‘s’. I no longer do so, as I feel that it is unnecessary, and adds little to the script’s readability. In fact it can interrupt the flow of reading a line of script. It seems to me that this is
30 Chapter 3: Variables - Variable Type not important. If you wish to distinguish between single-valued and array variables that’s fine, there doesn’t seem to be much of an advantage either way. As we will see later, variables used in sub-routines can be modified to provide something akin to scope. Choosing a good name for a variable requires some thought. While it becomes easier with practice, I still find the odd variable that seems to defy a concise name. Some strategies you might want to try are: using a dictionary or thesaurus, discussing the variable name with a colleague, or listing options to find the most suitable name. Often a good variable name will be obvious, but even then you should take some time to consider alternatives. And take care with the spelling. If you’re not naturally good at spelling use a dictionary or spell checker. Using the correct spelling just makes everything easier when you are debugging or maintaining software.
3.3 Variable Type A variable can be of one of three types: integer, real number, or text string. When you first set the value of a variable, you are in effect declaring its type. If a text value is initially assigned to a variable, it will be treated as text for the remainder of the script. Similarly, if an integer value is initially assigned to a variable, it will behave as an integer. In the case of integers, however, there is a slight difference. As a result of a calculation, or by direct assignment, a decimal number may be assigned to a variable that was initially an integer. From that point on, the script will treat the variable as a decimal number. Similarly, as soon as an integer value is assigned to a variable that was originally a decimal, it is thereafter treated as an integer. Actually, changing a variable type from a real number to an integer is slightly more involved than going the other way. To change a real number variable to an integer variable, you must either assign a constant integer value, the value of an integer variable, or an expression that must logically result in an integer value (e.g. the sum of two integer constants or variables). Try it Yourself: Set the Variable Type To illustrate the point, create a new object, and type the following GDL script into the 2D Script window. !Assign an Integer to a Variable j = 3 if j = 3 then print “Y”
Click on the Check Script button. A message will appear (figure 44), confirming that The GDL script is OK. Now change the script to read as follows.
Figure 44 – On checking the script, no errors were found.
Chapter 3: Variables - Variable Type 31 !Assign a Real Number to a Variable j = 3.1 if j = 3 then print “Y”
Click on the Check Script button. This time a warning message will appear (figure 45). Because the variable j is now considered to be a real number, it can no longer use the equals operator. Even if the value of j is set by a calculation that should numerically result in an integer value, it may still be interpreted as a real number. To see this, edit the script Figure 45 – On checking the script, an to read as follows. inconsistent subtype was encountered. !Variable Type is the Result of a Calculation j = 3.1/3.1 if j = 1 then print “Y”
On hitting the Check Script button, the exact same warning will pop up. The only way to re-type the variable as integer is to set its type using an expression that uses the int() command, or to set its value to an integer constant. The following scripts both illustrate ways of re-typing a variable to be an integer. Script 1: Use the int() command. !Change a Real Valued Variable to an Integer j = 3.1 j = int(j) if j = 3 then print "Y"
Script 2: Directly assign an integer value. !Change a Real Valued Variable to an Integer j = 3.1 j = 3 if j = 3 then print "Y"
3.3.1 Choose a Variable Type and Stick With It In summary, it is important to distinguish between real and integer values, as they behave differently in some circumstances. The biggest difference between them is that integers contain no round-off error, so they can be directly compared using the equals ‘=’ operator. While it is possible to change a variable’s type from integer to real and vice versa within a script, this is not often useful and can be extremely confusing. It’s better to initialize the variable, and stick with the initial type throughout the script. The only time this rule should be relaxed is if you initialize a real number with an integer – this is quite reasonable, as you will
32 Chapter 3: Variables - Array and Vector Variables often want to initialize the value to zero.
3.3.2 Change between Number & Text While it is not possible to re-type a number variable as a text string, you can convert numeric data to text data using the str( ) command. Similarly, it is possible to extract numeric data from a string using the split( ) command. These commands are both discussed later.
3.3.3 Parameter Variables When you set the type of your parameter variables, there are a number of options including fills, materials, line types, lengths and angles(figure 46). The number of parameter types seems to be greater than that of script variables which can be only Figure 46 – Choose a parameter integer, real or string. In fact, all parameter values are stored as either text, integer or real number. Material, fill type, line type, pen and Boolean parameters all have integer values. For the attribute parameters, only the index is actually stored. Boolean parameters are stored as 0 or 1.
type from the palette.
Length and angle parameters are stored as real variables. Although the value of a length parameter can display as millimeters, or as feet and inches, the variable value is stored in meters. Thus, a value of 1.04 in a GDL script means 1.04 m. 6 Angles are stored in units of degrees.
3.4 Array and Vector Variables Arrays are like tables of data. They are arranged in rows and columns. Each value in the table is identified by its row and column index. The value in the 3rd row and 6th column has the index [3][6]. If an array has only a single column, it may be referred to as a vector. In this context, a vector is a simple list of values. Each value is referenced by a single index. The first value stored in a vector is referred to by the index [1], the second by the index [2], and so on. Within a GDL script, you can define an array variable with either one or two dimensions using the dim command. !Define a 2D Array Variable of Fixed Size dim array_variable_name[number_of_rows][number_of_columns] !Define a 1D Array (Vector) Variable of Fixed Size
6
You can use inches as length units in a GDL script. To do so, write the value in decimal inches, then add an inch symbol. For example, 1.04’’ means 1.04 inches.
Chapter 3: Variables - Array and Vector Variables 33 dim vector_variable_name[number_of_rows]
To refer to one of the values of an array variable, specify the row and column in square brackets. For example, the following GDL script will set the value of the 3rd row and 2nd column of the array2D[][] variable to 14. !Assign a Value to an Array Element array2D[3][2] = 14
If you want the number of rows and columns to be extensible, don’t include any dimension values when you declare the arrays. Leave the brackets empty. If you do this, the array will automatically stretch to fit the values you set. !Define an Extensible 2D Array Variable dim array_variable_name[][] !Define an Extensible 1D Array (Vector) Variable dim vector_variable_name[]
Often you will want to check the size of a ‘stretchy’ (extensible) array. Use the vardim1() function to get the number of rows in the array, and the vardim2() function to get the number of columns. !Check the Number of Rows in an Array Variable number_of_rows = vardim1(array_variable_name) !Check the Number of Columns in an Array Variable number_of_columns = vardim2(array_variable_name)
Try it Yourself: Create an Array Variable, Set a Value, and Check the Array Size To see how this works, create a new object and type the script below into its 3D Script window. !Define a 2D Array Variable array2D[][] dim array2D[][] !Initialize one Element of the Array array2D[3][7] = 12 array2D[5][3] = 2 !Report the Array Size print “The size of the array is “, vardim1(array2D), “ x “, vardim2(array2D)
The script defines a stretchy 2-dimensional array and sets the values of two of its elements. It then prints a message that informs us of the size (number of rows and columns) of the array variable. Because we set an element in the 5th row of the array, and another element in the 7th column, the reported size of the array is 5 x 7, even though only two of the elements have been initialized.
3.4.1 Mixed-Type Arrays One of the cool things about GDL arrays is that the elements of the array variable can hold data of different types. In other words, one element of an array might be text, the next an integer, and the third a real number.
34 Chapter 3: Variables - Array and Vector Variables Note that this applies only to variables initialized within a GDL script. It does not apply to variables assigned to variables in the Parameters list. Try it Yourself: Create an Array that holds Elements of Different Types To see how this works, type the following script fragment into the 3D Script of a new GDL object. !Define a vector variable test[] dim test[] !Assign values to elements of test[] test[1] = "Hello World!" test[2] = 1.5 test[3] = 2 test[4] = "I pity the fool." !Report numeric data stored in variable test[] print test[2] + test[3] !Report string data stored in variable test[] print test[1] + " " + test[4]
Click on the 3D View button to run the script. A message will come up that reports a numeric value, then a message will come up reporting two text values. When are Mixed Type Arrays Useful? Normally it is not useful to store different data types in a single array, and it could be very confusing. Also, such an array has limited editing facilities unless the array element values are set to constant values as in our example. The main purpose of such an array is to store a temporary cache of data. Mixed-type arrays are particularly useful when retrieving data from a macro call or from a request. We will look at both scenarios in more detail later, but for now, the example below will illustrate how arrays can be used to get a bunch of useful data. Try it Yourself: Retrieve Project Storey Data in a Mixed-Type Array In this example, we use the request “Story_info” to populate a single array with integer, text and real data. We then distribute the data to four arrays that are designed to hold data about the storey indices, names, elevations and heights separately. The separated data is easier to work with. Create a new object, and copy the following GDL script into the 3D Script editing window. !Declare the array variables dim storeyInfo[], storeyIndex[],
Chapter 3: Variables - Array and Vector Variables 35 storeyName[], storeyElevation[], storeyHeight[] !Temporarily store storey info from the request in the storeyInfo array rrr = request("Story_info", "", nStories, storeyInfo) !Re-store the storey indices, names, elevations and heights in more appropriate arrays for i = 1 to nStories storeyIndex[i] = storeyInfo[4*(i - 1) + 1] storeyName[i] = storeyInfo[4*(i - 1) + 2] storeyElevation[i] = storeyInfo[4*(i - 1) + 3] storeyHeight[i] = storeyInfo[4*(i - 1) + 4] next i !Report the data print storeyIndex print storeyName print storeyElevation print storeyHeight
Click the 3D View button to run the script. A series of alert messages will come up. The first message will report a list of storey indices, the second a list of storey names, etc.
3.4.2 Copy Data Between Arrays You can copy blocks of data from one array to another. You can copy either a single array value, a complete row of data, or the entire array. To refer to a row of a two-dimensional array, specify only the row index. To refer to a complete array, leave the indices off altogether. Try it Yourself: Copy Data from One Array to Another Work through the following example to see how this works. Create a new object, copy the script into the 3D Script editing window, and click on the 3D View button to run the script. !Initialize the arrays dim initialArray[][], newArray[][] for i = 1 to 3 for j = 1 to 5 initialArray[i][j] = 10*i + j newArray[i][j] = 0 next j
36 Chapter 3: Variables - Scope next i !Copy the first row of initialArray to the third row of newArray newArray[3] = initialArray[1] !Report the values of newArray row by row for i = 1 to vardim1(newArray) print “Row “, i, “: “, newArray[i] next i !Copy the whole of initialArray to newArray newArray = initialArray !Report the values of newArray row by row for i = 1 to vardim1(newArray) print “Row “, i, “: “, newArray[i] next i
The ability to copy large chunks of data in a single line of code can be very convenient.
3.5 Scope A variable defined within a GDL script is confined to and global to that script. If you initialize a variable tubeRadius and give it a value of 0.025 in the 3D Script, for example, none of the other scripts can access its value. You can create a variable tubeRadius in the 2D Script, but this is treated as a totally independent variable with no link to the variable of the same name in the 3D Script. In effect, the GDL scripts are treated as independent entities. The only exception is the case of the Master Script. Since the Master Script is effectively run as a header to all the other scripts, any variable in the Master Script is global to all the other scripts. On the other hand, as soon as a variable has been initialized within a script, the remainder of the script (including subroutines) knows its type and value. A second variable of the same name cannot exist within the same script. This restriction may seem quite reasonable, as perhaps it is, but it does pose some interesting problems as we will see. Array variables have some interesting characteristics when it comes to scope. You must define an array before you can use it, which stands to reason. You can define an array variable in a sub-routine (we’ll discuss sub-routines later) but it’s best to define arrays in the main script. If you define an array variable in a sub-routine and then try to use it in the main script, error messages will pop up when you try to save the object. Try it Yourself: Investigate Scope and Array Variables To see what I mean, create a new object and copy the following script into the 2D Script editing window. !Initialize an array within a sub-routine gosub "Declare Array"
Chapter 3: Variables - Scope 37 !Try to use the array in the main script (i.e. physically above the sub-routine) arrayVariable[1] = 2 end "Declare Array": dim arrayVariable[] return
Click on the 2D Full View button to run the script. An error message will appear, “Non-declared indexed variable cannot be referenced at line 2 …”. Even though, logically, the array variable is already declared before the value 2 is assigned to it, the GDL interpreter gets confused. While it is not recommended to declare array variables in a sub-routine, the following change to the script will run fine. !Initialize an array in a sub-routine gosub "Declare Array" !Use the array in a second sub-routine physically below the first gosub “Use Array” end "Declare Array": dim arrayVariable[] return “Use Array”: arrayVariable[1] = 2 return
The variable must be declared at a point in the script that comes both physically and logically ahead of the point at which it is used. If it is declared in line 106 of the script, it cannot be used in line 45. In the second example, the variable was declared at line 5 and used at line 8. If the order of the sub-routines was reversed, the script would again fail to run.
4
Operators and Expressions
Now that you know how to create variables and parameters, you will want to change and compare their values, and do other useful stuff with them. Most of the operators such as (+, -, /, *, ^) are quite intuitive. Obviously, using * for multiplication is just a matter of syntax, as is the use of ^ to raise a number to a power, but the operators themselves work exactly as you would expect them to. Similarly, the absolute value function abs() holds no surprises. It simply returns the absolute value of whatever expression happens to be inside the brackets. The same could be said for the trig functions sin(), cos() and tan() and their inverses asn(), acs() and atn(). These work just like the functions on a calculator. The exp() function simply returns the transcendental number e raised to the power of the expression in the bracket. Our old friend the equals operator ‘=’ is a bit more interesting, and we’ll look at this operator in some detail. The less than operators ‘<’ and the greater than operator ‘>’ are used to check if the left value of an inequation is less than or greater than the right value. The result is either 1 (true) or 0 (false). These can be used in tandem with the equals operator to provide less than or equal to ‘<=’ and greater than or equal to ‘>=’ operators. The modulus operator mod() or % returns the remainder of the division of one number by another. For example, the expression 63 % 5 evaluates to 3, since 63 = 12*5 + 3. The operator can be used for real and negative numbers. There are a bunch of other operators: •
sgn() returns the sign of the expression in the brackets (-1, 0 or 1 for negative, zero and positive values).
•
int() returns the integer part of a real number (e.g. int(pi) = 3)
•
fra() returns the fractional part of a real number (e.g. fra(pi) = 0.14159…)
•
ceil() returns the next highest integer (e.g. ceil(pi) = 4)
•
sqr() returns the square root of an expression.
Finally, there are a few interesting operators that do cool stuff that you may not be so familiar with. Once we’ve considered some of the more interesting operators in detail, we’ll take a look at how they can be used to form expressions and to actually write some lines of GDL code.
4.1 The Equals Operator The equals operator ‘=’ has two functions. As we have already seen, it can be used to assign a value to a variable. For
Chapter 4: Operators and Expressions - The Equals Operator 39 example, the following script assigns the value 5 to the variable a. !Use the Equals Operator to Assign a Value to a Variable a = 5
When used as part of an expression, the equals operator can also be used to compare two values. The following script fragment compares the name of a regular polygon and assigns the number of edges to the variable nEdges. !Determine the number of edges of a polygon if pgonName = “Triangle” then nEdges = 3 endif if pgonName = “Square” then nEdges = 4 endif if pgonName = “Pentagon” then nEdges = 5 endif
In the example above, notice how the equals operator is used to compare the value of pgonName with a constant string value. Because the equals operator is used within an expression, the constant value is not assigned to the variable. Instead, the equals operator compares the two values and returns 1 if they match exactly, and 0 if they differ. The equals operator can be used this way within any expression. The script below uses the equals operator inside a bracket of a regular calculation, as well as assigning the result of the calculation to a variable. nEdges = 0 nEdges = nEdges + 3*(pgonName = “Triangle”) nEdges = nEdges + 4*(pgonName = “Square”) nEdges = nEdges + 5*(pgonName = “Pentagon”)
This script does exactly the same as the previous example. If the value of variable pgonName is “Square”, for instance, the second and fourth lines of code will add zero, while the third line of code will add 4. Thus the value of nEdges will be evaluated as 4. Comparing Real-Number Values You can use the equals operator to directly compare the values of two strings or two integers (including materials, fills and line types). Real number values (including lengths and angles) are a different kettle of fish. The stored value of a real number contains a large number of decimal places. As a result, two numbers that differ after the 5th decimal place are practically equal (a hundredth of a millimeter is an acceptable tolerance in the building industry). Rather than using the equals operator to directly compare two real numbers, we should instead test whether the
40 Chapter 4: Operators and Expressions - The Inequality Operator difference between the values is significant. For example, the following (useless) script compares two real numbers. !Check if two Real Numbers are Equal tol = 0.00001 if abs(a – 1.3) < tol then print “The length is 1.3m” endif
In this script, the length-type variable a is compared with the value 1.3. If the difference between the two values is less than 0.01mm, then the result of the comparison is true, and the message will be printed. Note that we used the absolute value in this script. By doing so we allow a slight deviation or tolerance on either side.
4.2 The Inequality Operator The inequality operator (# or <>) compares two given integer or string values. If the values differ it returns 1 (true). Otherwise it returns 0 (false). As with the equals operator, the inequality operator cannot be used to directly compare two real numbers. Instead, we must test whether the absolute value of the difference is significant. The following line of GDL script demonstrates how to check for inequality in the case of strings, integers and real numbers. !Tolerance tol = 0.0001 !Initialize Variables !Strings stringVar1 = “Hello World” stringVar2 = “Farewell, cruel World” !Integers intVar1 = 13 intVar2 = 13 !Reals realVar1 = 3.14159 realVar2 = pi !Check if the string variables are different if stringVar1 # stringVar2 then print “The string variables are not identical.” endif !Check if the integer variables are different
Chapter 4: Operators and Expressions - Brackets and the Order of Operations 41 if intVar1 # intVar2 then print “The integer variables are not identical.” Endif !Check if the real variables are different if abs(realVar1 - realVar2) > tol then print “The real variables are not identical.” endif
See how real variables must be treated differently to integers and text string variables. Very fussy creatures.
4.3 Brackets and the Order of Operations It is important to be aware of the order of operations. When the GDL interpreter compiles the script, it performs operations in a well-defined order. Expressions enclosed in round brackets () are evaluated first. Then come in order of operation: 1.
Exponents or powers ^, **.
2.
Multiplication *, division / and modulus %, mod.
3.
Addition + and subtraction -.
4.
Equals =, does not equal #, is greater than >, is less than <, is greater than or equal to >=, is less than or equal to <=.
5.
Logical AND, and, &.
6.
Logical OR, or,|.
7.
Logical ‘exclusive or’ EXOR, exor, @.
When operators of identical order are written in line (such as 3*5/2), the operations are carried out from left to right.
4.3.1 Use Brackets to Control the Order of Operation It is often necessary to control the precise order in which terms in an expression are evaluated. Imagine that you want to calculate the area of an oversized rectangular window opening. The width of the window is A = 2.4m and the height B = 0.9m. The oversize on each side (head, sill and jambs) is 5mm. Now the area of the rectangle is the base times the height. The base is A + 0.010, and the height is B + 0.010. To calculate the area we must first calculate the base, then the height, and finally multiply the results.
42 Chapter 4: Operators and Expressions - Logical Operators Let’s say that we were to write out the calculation as shown below. !Calculate the Area windowArea = A + 0.010 * B + 0.010
The order of operations would dictate that the multiplication be performed first, followed by the additions,. The area would thus be calculated (incorrectly) as 2.4 + 0.009 + 0.010 = 2.419m2. In order to force the two additions to be evaluated first, we enclose them in brackets. !Calculate the Area windowArea = (A + 0.010)*(B + 0.010)
The expressions in the brackets are evaluated first, and the results are then multiplied to give the correct value for the area – 2.410*0.910 = 2.1931m2.
4.3.2 Use Brackets to Communicate As we will see later, it is essential that any script you write can be easily understood by other programmers. So use brackets wherever there is any doubt as to which expression will be calculated first. Even where they are not strictly necessary, they visually indicate the order in which the operations will be carried out. This is especially important when you use a very complex expression.
4.3.3 Nested Brackets If you nest brackets, the inner brackets will be calculated before the outer brackets. For example, in the script below, the first terms to be evaluated are (x – xCenter) and (y – yCenter). Then the outer bracket is evaluated (the terms are squared then added), and finally the square root of the result is assigned to the variable L. !Calculate the Radius L = sqr((x – xCenter)^2 + (y – yCenter)^2)
4.4 Logical Operators Logical operators can be used to assemble logical expressions. These expressions may be evaluated as either true or false. If a logical expression is evaluated to be false, it returns 0. If true, it returns 1. Similarly, any expression that returns zero is considered logically false. Any expression that returns a non-zero value (this does not have to be 1 – any non-zero value will do) is considered logically true.
4.4.1 The ‘If- Then’ Condition The if - then statement is a switch that controls whether or not a block of script will run. The argument of the if statement is a logical or arithmetic test expression. If the test expression is evaluated to be true
Chapter 4: Operators and Expressions - Logical Operators 43 (strictly non-zero), the block of script will run. If the test expression is evaluated to be false (strictly zero), then the GDL interpreter will skip over the block of script and move on to the following line. The test expression is sandwiched between the if and the then. The script block follows, and the end of the block is marked with the key word endif. !Syntax for the ‘if-then’ condition if test_expression then script_block endif
In many cases, one block of script will be required if the test expression is true, and another if it is false. Use the else condition to split the two blocks of script. !Syntax for the ‘if-then-else’ condition if test_expression then script_block_1 else script_block_2 endif
Where the blocks of script enclosed by an if … else … endif statement are relatively large, or when they should be clearly commented, I tend to avoid using the else statement. Instead I set an integer parameter to hold the result of test_expression, and create two if … then statements as follows. !Alternative approach to the ‘if-then-else’ condition testValue = test_expression !Comment 1 if testValue then script_block_1 endif !Comment 2 if not(testValue) then script_block_2 endif
A similar approach works well if multiple test_expressions need to be evaluated. !Syntax for an if-then expression that can be true for multiple tests testValue = 0 if test_expression_1 then testValue = 1 if test_expression_2 then testValue = 1 if test_expression_3 then testValue = 1
44 Chapter 4: Operators and Expressions - Logical Operators if testValue then script_block endif
4.4.2 The ‘and’ or ‘&’ Operator Use the and operator to check whether two test expressions are simultaneously true. !Syntax for the ‘and’ operator if test_expression_1 and test_expression_2 then script_block endif
If you like using shorthand, you can use the & character in place of the word and. This still makes for a readable script since we are all familiar with the ampersand character. Both forms are treated the same. !Syntax for the ‘&’ operator if test_expression_1 & test_expression_2 then script_block endif
4.4.3 The ‘not()’ Operator Use the not() operator to test whether an expression is false. !Syntax for the not() operator if not(test_expression) then script_block endif
4.4.4 The ‘or’ or ‘|’ Operator Use the or operator to check whether at least one of two test expressions is true. !Syntax for the ‘or’ operator if test_expression_1 or test_expression_2 then script_block endif
If you like using shorthand, you can use the | character in place of the word and. Both forms are treated the same. !Syntax for the ‘|’ operator if test_expression_1 | test_expression_2 then script_block endif
Chapter 4: Operators and Expressions - Bit Operators 45 I prefer to use the full word. My reason for this preference, is that it is important to make your script as easy to read as is possible. It probably won’t always be you who has to maintain your scripts. So the more consistent and easily understood they are, the better it is for everyone. When given the choice between using a perfectly understandable word, or a cryptic symbol, the choice seems obvious. 7
4.4.5 The ‘exor’ or ‘@’ Operator Use the or operator to check whether exactly one of two test expressions is true. If both expressions are true, the exor operator will return zero (false). !Syntax for the ‘exor’ operator if test_expression_1 exor test_expression_2 then script_block endif
If you like using shorthand, you can use the @ character in place of the word exor. !Syntax for the ‘@’ operator if test_expression_1 @ test_expression_2 then script_block endif
Both forms are treated the same, but for the reasons given I would suggest using the longhand version for the sake of readability. You could survive for years without using the exor operator more than a handful of times.
4.4.6 Using Mixed Logical and Algebraic Expressions Typically, logical expressions are used in conditional if – then statements. However, because they return a numerical value (1 or 0), such expressions can be used perfectly well as part of a regular algebraic expression. Conversely, algebraic expressions can be used as the arguments of if – then statements.
4.5 Bit Operators Though not so frequently used as algebraic and logical operators, bit operators can be very useful in some situations. These operate on the bits of stored data – the binary ‘ones’ and ‘zeros’ that the computer uses to store data. When the computer stores a number, it uses a base two (binary) system. For example, the number 37 can be written as 32 + 4 + 1 = 25 + 22 + 7
OK if you are a C++ programmer the choice may not seem so obvious. But bear in mind that many GDL programmers are self-taught and come from an Architectural rather than programming background.
46 Chapter 4: Operators and Expressions - Bit Operators 20. A binary representation of this number would be 100101. Each digit of the binary number is a bit. The 0th, 2nd and 5th bits of this number are set to 1 and the remaining bits are set to zero. Bit operators can be used to store multiple binary data in a single integer. I often use bit operators for values lists, where a number of yes/no choices can be set by a single selection from a list. For instance, when using the Cadimage Wall Covering, you get to break the wall into regions. You can choose to use a different cladding above and below a speficied sill height, above and below a specified head height, above openings, and below openings (figure 47). This all boils down to a series of ‘yes/no’ questions such as “Do you want to split the wall at sill height?”, “Do you want to split the wall at head height?”, “Do you want a different cladding type above openings?”, “Do you want a different cladding type below openings?” By treating each answer as a 1 (yes) or 0 (no), and storing all the answers as a single binary number (eg. 5 = binary 0101 would mean “split at sill height and above openings “, while 2 = binary 0010 would mean “split above head height”) we can present all the possible choices in a single integer-type variable. Figure 47 – Bit operators are used together with
this selection control from Cadimage’s Wall
For the user, this translates to a single selection from a list, rather than Covering tool. clicking on 4 checkboxes. The available values would be 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15. The first value 0 is binary 0000 which means that a single cladding will be applied to the whole wall. The final value 15 is binary 1111, which means that the wall will be split at both sill and head heights, and have different claddings above and below the windows. In regular GDL, bitwise data is often used in the masks of various geometric elements such as prism_, tube and poly2_. Bitwise data is also used to define status codes for polylines.
4.5.1 Setting Bit Values You can set a particular bit of a numeric variable to be either 0 or 1. This can be useful for setting status values, where each bit of the number sets a particular switch. Use the bitSet operator to do this. !Set the ‘bit_to_set’th bit of variable_1 and assign the result to variable_2
Chapter 4: Operators and Expressions - Bit Operators 47 variable_2 = bitset(variable_1, bit_to_set, 1)
This operator sets the result variable_2 to be the value of variable_1 modified by having its bit_index_to_setth bit set to 1. If you just want to adjust a given variable’s value, use the same variable in each slot. !Set the ‘bit_to_set’th bit of variabl_1 and assign the result to variable_1 variable_1 = bitset(variable_1, bit_to_set, 1)
You can also set a bit value to zero: !Un-set the ‘bit_to_set’th bit of variable_1 and assign the result to variable_2 variable_2 = bitset(variable_1, bit_index_to_set, 0)
Remember that the first bit has a zero index, since 20 = 1. Try it Yourself: Set and Un-set Bits of a Variable To see how this works, create a new GDL object and copy the following script into its 3D Script editing window. !Initialize Variable n to Zero n = 0 !Initialize variable m to be the same as variable n but set the 0th bit to 1 m = bitset(n, 0, 1) !Now set the 3rd bit of variable m to 1 m = bitset(m, 3, 1) !Report the value of variables m and n print m, n !Set variable n to 31 n = 31 !Set variable m to match variable n, but set the 4th bit to 0 m = bitset(n, 4, 0) !Report the value of variables m and n print m, n
Click on the 3D View button to run the script. The first print statement should report “9, 0”, since the zeroth (20 = 1) and 3rd (23 = 8) bits of m have been set to 1. The second print statement should report “15, 31”, since the value of m is the value of n but with the 4th bit (24 = 16) set to zero.
4.5.2 Testing Bit Values Similarly, you can use the bitTest operator to check the bit status of a variable. rrr = bittest(test_variable, bit_index_to_test)
48 Chapter 4: Operators and Expressions - Bit Operators The operator returns 1 if the bit_index_to_testth bit is set, otherwise it returns zero. Try it Yourself: Test the Bits of a Variable Harking back to the example of the Cadimage Wall Covering, we’ll create a ‘dummy’ object that checks the input values and reports how the wall will be broken down into regions of cladding. Create a new object, and add an integer parameter with variable claddingSetout. In the Parameter Script, add the following code: !Available values for the Cladding values
“claddingSetout” 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
In the 3D Script editing window, copy the following script block: report = “” !Case 1: Complete cladding if claddingSetout = 0 then report = “A single cladding will be applied to the whole wall.” else report = “The cladding will be broken into regions as follows: ” endif !Check for a split at sill height if bittest(claddingSetout, 0) then report = report + “Split at Sill Height, “ !Check for a split at head height if bittest(claddingSetout, 0) then report = report + “Split at Head Height, “ !Separate cladding above openings if bittest(claddingSetout, 0) then report = report + “Separate Cladding above Openings, “ !Separate cladding above openings if bittest(claddingSetout, 0) then report = report + “Separate Cladding below Openings, “ !Report the value of variables m and n print report
Go to the Parameters list, and select a value for the claddingSetout parameter. Click on the 3D View button to run the 3D Script. A message will come up, describing what regions the wall claddings will be broken into.
5
Working with Text Strings
Data that consists of a string of text characters are referred to simply as strings. You can define a constant string by including the characters in quotation marks 8 as in “The quick brown fox jumps over the lazy dog”. A string can be assigned to a text type variable, and used in expressions. Strings are used in various ways by GDL objects. This chapter discusses some of the operations that can be used when working with strings. First, I’ll explain how to count the number of characters in a string, how to search for one string within another, and how to read part of a string. We’ll use these operations to develop a method for substituting one character sequence for another. We’ll see how to turn numeric data into a formatted string, and how to create a formatted string from the current date and time. Finally, we’ll look at how to extract numeric data from a string. By which time you will know everything there is to know about strings in GDL.
5.1 Number of Characters in a String To find the number of characters in a given text string, use the strlen() function. The returned value is the number of characters, including blank spaces and punctuation symbols. Figure 48 – The warning Try it Yourself: Report the Number of Characters in a String To see how this works, create a new object and copy the following script into the 3D Script editing window.
message reports the number of characters in the string.
!Report the length of a string textVar = “The quick, brown fox jumps over the lazy dog.” print strlen(textVar)
Run the script by clicking on the 3D View button. A warning message will pop up reporting the number of characters in the string (figure 48).
8
Take care when copying code from other text editors, as not all variations of quotation marks are recognized by the GDL interpreter.
50 Chapter 5: Working with Text Strings - Search for a Text within a String
5.2 Search for a Text within a String To find whether one string contains another, use the strstr() function. The returned value is the index in the parent string where the first occurrence of the search string is located. If no match is found, the returned value is zero. If more than one match is found, only the position of the first occurrence is returned. Try it Yourself: Report where One String is Found within Another To see how this works, create a new object and copy the following script into the 3D Script editing window. !Report the location of a word within a string textVar = “The quick, brown fox jumps over the lazy fox.” print strstr(textVar, “fox”), strstr(textVar, “g”)
Run the script by clicking the 3D View button. A message will pop up that reports the index of the first occurrence of the word ‘fox’ (figure 49). The first occurrence of the character sequence ‘fox’ starts at the 18th character of the string. The character ‘g’ does not occur at all in the string, so the operator returns zero.
Figure 49 – The warning message reports the position of the first occurrence of the string ‘fox’.
5.3 Read Part of a String If you need to know the character sequence of part of a string, use the strsub() operator. This operator takes a string expression, a start point and the number of characters you want to read, and returns the extracted substring. Try it Yourself: Report Part of a String To see how this works, create a new object and copy the following script into the 3D Script editing window. !Report a part of a string textVar = “The quick, brown fox jumps over the lazy dog.” print strsub(textVar, 37, 8)
Run the script by clicking the 3D View button. A message will pop up that reports the 8letter character sequence starting at the 37th character of the text string (figure 50).
5.4 Substitute Text One way to make practical use of the string functions is to provide some ‘auto-text’
Figure 50 – The warning message reports the 8-character long portion of the string starting at the 37th character.
Chapter 5: Working with Text Strings - Display Numeric Data as a String 51 functionality. Key words can be added into a label, for example, by the user. These words are detected by the GDL script, and values substituted in their place. For example, a door marker might use the key word to stand for the height of the leaf. The GDL script would then automatically replace the keyword with formatted dimension text. Text substitution involves two stages. First we have to check that the key word exists in the string. Then we have to replace the keyword with the substitute text. Of course, the keyword may be used more than once in the string, so we should really repeat the process until all instances have been used. For example, to substitute subText for keyWord in a string variable stringVar, you could use the following script fragment. !Substitute each instance of keyWord with subtext keyWordLength = strlen(keyWord) subPos = strstr(textVar, keyWord) while subPos do textVar = strsub(textVar, 1, subPos - 1) + subText + strsub(textVar, subPos + keyWordLength, strlen(textVar) - keyWordLength) subPos = strstr(textVar, keyWord) endwhile
It would be a good idea to check that the values of keyword and subtext are different, or the loop will churn on for eternity (or until your computer breaks down). Let’s modify the script slightly: !Substitute each instance of keyWord with subtext if keyword # subtext then keyWordLength = strlen(keyWord) subPos = strstr(textVar, keyWord) while subPos do textVar = strsub(textVar, 1, subPos - 1) + subText + strsub(textVar, subPos + keyWordLength, strlen(textVar) - keyWordLength) subPos = strstr(textVar, keyWord) endwhile endif
5.5 Display Numeric Data as a String To display numeric data as part of a string, use the str() function.
5.5.1 Decimal Numbers Decimal numbers can be displayed using the function:
52 Chapter 5: Working with Text Strings - Display Numeric Data as a String !Syntax to create a string that displays a formatted decimal number str(numeric_expression, minimum_number_of_digits, number_of_decimal_places)
For instance, you could use the following GDL script to display the first two decimal places of the numbers pi ( 3.14159265… ) and 100*pi ( 314.159265…). !Report pi and 100*pi to 2 decimal places print str(pi, 3, 2), str(100*pi, 3, 2)
Try it Yourself: Convert a Number to a String Create a new object, and type the above script into the 3D Script editing window. Click on the 3D View button to see the result. Note that the second argument in both instances of the str() operator is 3. This is the minimum number of digits that will be displayed. For Figure 51 – The number pi the larger number (314.159265…) the number of digits is automatically increased to 5.
5.5.2 Special Formatting
displayed in two formats.
You can format numeric data to display in other ways, using a different version of the str() function. A typical use for this is to display a number in special units such as feet and inches, millimeters, degrees or square feet. !Syntax to create a string that displays a number with special formatting str(number_format_string, numeric_expression_to_display)
The format string is made up of keywords and other text that will be inserted into the number. It can be either a variable or a constant. By default the number is formatted in meters with an accuracy of 3 decimal places, and displaying a leading zero whole number where required. The structure of the format string is: %[flags][field_width][.precision]conversion_unit
The flags field may include: •
A flag to left-justify the number (by default the number will be right-justified).
•
For decimal numbers (i.e. not feet and inches) o
a + flag or a [blank space] flag to display a plus sign explicitly before positive numbers, or a blank space before positive numbers.
o
a ~ flag will hide zero decimals
•
A # flag if you don’t want to display zero wholes.
•
If you chose ffi, fdi or fi, a 0 flag will display zero inches.
The field_width value is an integer specifying the minimum number of characters to generate.
Chapter 5: Working with Text Strings - Display Numeric Data as a String 53 The precision is an integer specifying the number of fraction digits to generate. For example, type .64 if you want fractions accurate to 1/64”, or .8 for fractions accurate to 1/8”. Note the leading decimal point. The conversion_unit value is a conversion specifier. This specifies what units you want to convert the value into. The original number is given in meters, square meters, cubic meters or degrees. When you specify the conversion_unit, the value will be automatically converted to the chosen units. The following table lists available format strings. Length Units e exponential format m meters mm millimeters cm centimeters ffi feet & fractional inches
fdi df
fi di
feet & decimal inches decimal feet
fractional inches decimal inches
Area Units sqm square meters sqcm square centimeters sqmm square millimeters sqf square feet sqi square inches
Angle Units dd decimal degrees dms degrees, minutes, seconds gr grads rad radians surv surveyors unit
Volume Units cum cubic meters cucm cubic centimeters cumm cubic millimeters cuf
cubic feet
cui cuy
cubic inches cubic yards
l
liters
gal
gallons
Try it Yourself: Format a Dimension as Feet and Fractional Inches To see how this special formatting works, create a new object and copy the following GDL code into its 2D Script editing window. In the Parameters list, set the value of A to 1.000 m. !Text Style define style "tx" Arial, 2, 1, 0 style "tx" !Convert A to feet and fractional inches tx = str("%.32ffi", A) !Display the result text2 0, 0, tx
Figure 52 – The 2D preview shows a numeric value
Click on the 2D Preview button to display the result (figure 52). Try formatted as feet and fractional inches.
54 Chapter 5: Working with Text Strings - Display Numeric Data as a String changing the precision value from .32 to .4, and click on the 2D Full View button to view the change.
5.5.3 Match the Project Dimension Formats For each ArchiCAD project, the preferred formats for displaying dimensions are set in the Options > Project Settings > Dimensions palette (figure 53). You can access these display formats, and use them in the str( ) function to re-produce the exact same format in your GDL objects. These formats can be obtained using request functions. Length Area Volume Angle
rrr = request(“Linear_dimension”, “”, linearDimFormat) rrr = request(“Area_dimension”, “”, areaDimFormat)
Figure 53 – Use the Project Preferences dialog to set the project dimension format.
rrr = request(“Volume_dimension”, “”, volumeDimFormat) rrr = request(“Angular_dimension”, “”, angularDimFormat)
The returned values are format strings as discussed in the preceding section. You can use the results directly in the str() operator. Try it Yourself: Apply Project Dimension Formatting
Figure 54 – Project formatting typical of To see this in action, create a new object, and save is as Test Dimension Format. Add a metric template.
the following parameters. •
A length type parameter with variable lengthVal.
•
An angle type parameter with variable angleVal.
•
A real number type parameter with variable areaVal.
Type the following script into the object’s 2D Script editing window. !Text Style define style "tx" Arial, 2, 1, 0 style "tx" !Line spacing
Figure 55 – Use the Project Preferences dialog to adjust dimension formats.
lineSpacing = 3*glob_scale/1000 !Get current dimension formats rrr = request(“Linear_dimension”, “”, linearDimensionFormat) rrr = request(“Area_dimension”, “”, areaDimensionFormat) rrr = request(“Angular_dimension”, “”, angularDimensionFormat) !Display the formatted numbers
Figure 56 – The object updates to use the new project dimension formatting.
Chapter 5: Working with Text Strings - Display the Current Date and Time as a String 55 text2 0, 0, str(linearDimensionFormat, lengthVal) text2 0, -lineSpacing, str(areaDimensionFormat, areaVal) text2 0, -2*lineSpacing, str(angularDimensionFormat, angleVal)
Save the object, and place an instance onto the project’s plan view window (figure 54). Now change the dimension format settings for length, angle and area (figure 55). The object will immediately update to display the new formats (figure 56).
5.6 Display the Current Date and Time as a String In some cases you may want to display the current date or time as a string. You can use the DateTime request to return the current time as a string. The request is written as follows. rrr = request(“DateTime”, format_string, date_time_variable)
The resulting time string will be returned in the string variable date_time_variable. Specify the format of the returned string using the format_string expression. This may be any text string that you want. Include the current date and time by inserting key words into the format_string expression. These will be substituted by the current date and time data. Available key words for use in the format_string are listed below. Year
Time
%Y
year with century e.g. 2008
%P
AM/PM
%y
year without century (00-99) e.g. 08
%H
hour (24-hour clock), an integer (00-23)
Month
%I
hour (12-hour clock), an integer (01-12)
%B
full month name
%M
minute, an integer (00-59)
%b
abbreviated month name
%S
second, an integer (00-61)
%m
month, an integer (01-12)
Full Date and Time Formats
Week %U
week number of the year (Sunday the first day of the first week), an integer (00-53)
%x
date eg. Wednesday, March 27, 1996
%X
time eg. 01:35:56 PM
56 Chapter 5: Working with Text Strings - Display the Current Date and Time as a String %W
week number of the year (Monday the
%c
first day of the first week), an integer
date and time eg. 01:35:56 PM Wednesday, March 27, 1996
(00-53) Day %A
full weekday name
%a
abbreviated weekday name
%w
Weekday (0 = Sunday - 6 = Saturday)
%d
day of the month, an integer (01-31)
%j
day of the year, an integer (001-366)
Try it Yourself: Display the Current Date and Time To see how this works, create a new GDL object and type the following GDL script into its 2D Script editing window. !Text Style define style "tx" Arial, 2, 1, 0 style "tx" !Line spacing lineSpacing = 3*glob_scale/1000 !Get the current date and time as formatted strings rrr = request(“DateTime”, “%x”, currentDateStandard) rrr = request(“DateTime”, “%A %d/%m/%Y”, currentDateCustom) rrr = request(“DateTime”, “%X”, currentTimeStandard) rrr = request(“DateTime”, “%H:%M:%S”, currentTime24Hour) !Display the formatted dates and times text2 0, 0, currentDateStandard text2 0, -lineSpacing, currentDateCustom text2 0, -2*lineSpacing, currentTimeStandard text2 0, -3*lineSpacing, currentTime24Hour
Click on the 2D Full View button to run the script. As you can see (figure 57), regular text can be included with the keywords. In the example, we added colons, slashes and a blank space to provide a sensible format for the custom date and the 24 hour time.
Figure 57 – The 2D script places formatted date and time strings.
Chapter 5: Working with Text Strings - Extract Numeric Data from a String 57
5.7 Extract Numeric Data from a String Sometimes it is necessary to extract numeric data from a string. This could happen, for example, if you allow users to input a text height as a string including the units. To split a string into numeric and string data, use the split() operator. nnn = split(string_expression_to_split, split_format, value_1, value_2, value_3, … )
The string_expression_to_split value may be any string type expression. The split_format is a string expression that may contain the key words %s and %n to represent string and numeric data. It can also include separators if required. The value_s are the returned string and numeric data into which the string is split. The returned value nnn is the number of found string and numeric values. Try it Yourself: Extract Numeric Data and Use it to set the Text Height Create a new object and add two new text parameters with variables textHeight and textHeightPrev. Copy the following script into the Parameter Script editing window. !Check that the input text height includes number and unit unit = "" nnn = split(textHeight, "%n%s", height, unit) !If the split is successful, standardize the input data if nnn = 2 then !Remove all blank spaces from the unit includesBlankSpace = strstr(unit, " ") while includesBlankSpace do unit = strsub(unit, 1, includesBlankSpace - 1) + strsub(unit, includesBlankSpace + 1, strlen(unit) - includesBlankSpace) includesBlankSpace = strstr(unit, " ") endwhile !Standardize the unit if strstr(unit, "P") or strstr(unit, "p") then unit = "pt" else unit = "mm" endif !Standardize the textHeight value textHeight = str("%~2.1", height) + " " + unit
58 Chapter 5: Working with Text Strings - Include a Quotation Mark in a String Constant !Calculate the text height in mm if unit = "pt" then textHeightmm = height if unit = "mm" then textHeightmm = height !Store the values parameters textHeight = textHeight, textHeightPrev = textHeight, textHeightmm = textHeightmm else !Else, revert to the previous text height textHeight = textHeightPrev parameters textHeight = textHeightPrev endif
Now click on the Parameters button to view the Parameters list, and try typing in some values for the textHeight parameter. You will see how the Parameter Script runs each time to interpret the input value as either points or millimeters. If the Parameter Script cannot successfully Figure 58 – Enter the text height including unit as a string. interpret the data, it will reverts back to the previous value.
5.8 Include a Quotation Mark in a String Constant String constants are always inserted within quotation marks, which makes it awkward if you want to include a quotation mark as part of the string. It turns out that the GDL interpreter can recognize two types of quotation mark. Single parentheses and double parentheses are both interpreted as quotation marks.To include a quotation mark in a string, then, enclose the string in single parentheses. Try it Yourself: Include Quotation Marks within a String For example, try running the script below. !Include a double quotation mark in a string print
‘Include a “quotation mark” in a string constant.’
!Include a single quotation mark in a string print “Include a ‘quotation mark’ in a string constant.”
6
Using the Stack
The stack is a memory structure into which you can insert, and from which you can retrieve, temporary data. Data is added at the end of the stack and retrieved from the start, so that it is retrieved in the same order in which it was added. For example, if you put the numbers 1, 2, 3, 4, 5 into the stack in that order, you can retrieve them in the same order – 1, 2, 3, 4 and 5. You can only store numeric data on the stack. In this short chapter we’ll look at the stack functions, and consider how the stack can be used to advantage. We will also consider some common pitfalls that can occur when using the stack, and discuss strategies for avoiding these.
6.1 Put, Use and Get Values To put data onto the stack, use the put command followed by comma-separated list of the numeric data you want to store. !Syntax to store data on the stack put numeric_data_1, numeric_data_2, numeric_data_3, …, numeric_data_n
To retrieve data from the stack, use either the get or use command followed by the number of data you want to retrieve. The difference between getting and using data is that getting data removes the data from the stack, while using data leaves the stack intact. If you use the get function, the process of retrieving values removes them from the stack. This can be handy as it clears the stack. The use function leaves the values on the stack. This can be handy if you want to use the same data multiple times. For instance if you are placing a number of identical polygons, you need only calculate the vertices once, and you can use them as many times as you like. The variable nsp holds the number of data currently on the stack. Try it Yourself: Use the Put, Use and Get and nsp Commands To get a handle on how this works, create a new GDL object and copy the following script into its 3D Script editing window. !Put data on the stack for i = 1 to 10 put i
60 Chapter 6: Using the Stack - Use ‘Put’ and ‘Get’ to Simplify Scripts next i !Print the data without removing it from the stack print use(10) !Print some of the data while removing it from the stack print get(3) !Print all the remaining data and clear the stack print get(nsp)
Run the script by clicking on the 3D View button.
Figure 59 – The first warning message reports values from the stack without removing those values from the stack.
In this example, the loop places the integers 1 to 10 onto the stack. The first print statement reports all 10 numbers (figure 59). Because it is a use rather than a get, the data remains on the stack. In the second print statement, the three reported numbers are 1, 2 and 3 Figure 60 – The second warning message reports the (figure 60). Now these have been removed from the stack by the get first three values from the stack, and removes those function. values. The final print reports all remaining data on the stack – in our case integers 4, 5, 6, 7, 8, 9, 10 (figure 61). At this point the stack has been completely cleared.
6.2 Use ‘Put’ and ‘Get’ to Simplify Scripts Multiple data can be retrieved directly from the stack and inserted into a GDL element. For example, you could put all the vertices of a polygon onto the stack and then inject them directly into the poly2 command. In Figure 61 – The last warning message reports the practice, this means you can use an iterative method to put the data. remaining stack values, and clears the stack. This is true of many of the GDL commands, such as poly2_b, prism_, rotate and tube, which all require a set of numerical data. Frequently, some or all of this data can be generated by applying an iterative rule. This can cut down on the scripting required. Try it Yourself: Draw a Lissajous Curve Let’s draw a Lissajous curve such as would be used to indicate a cylinder break. Parametrically, this curve might be described as:
= x (θ ) cos( = θ) y (θ ) sin(2θ )
Chapter 6: Using the Stack - Use ‘Put’ and ‘Get’ to Simplify Scripts 61 To manually script a polygon that traces this curve through 270º, we would have to decide on a step size for θ, say 5º. The script would look something like this. !Cylinder Break Line R = a/2 poly2_ 55, 1, R*cos(0), R*sin(2*0), 1,
R*cos(5), R*sin(2*5), 1,
R*cos(10), R*sin(2*10), 1,
R*cos(15), R*sin(2*15), 1,
R*cos(20), R*sin(2*20), 1,
R*cos(25), R*sin(2*25), 1,
R*cos(30), R*sin(2*30), 1,
R*cos(35), R*sin(2*35), 1,
R*cos(40), R*sin(2*40), 1,
R*cos(45), R*sin(2*45), 1,
R*cos(50), R*sin(2*50), 1,
R*cos(55), R*sin(2*55), 1,
R*cos(60), R*sin(2*60), 1,
R*cos(65), R*sin(2*65), 1,
R*cos(70), R*sin(2*70), 1,
R*cos(75), R*sin(2*75), 1,
R*cos(80), R*sin(2*80), 1,
R*cos(85), R*sin(2*85), 1,
R*cos(90), R*sin(2*90), 1,
R*cos(95), R*sin(2*95), 1,
R*cos(100),R*sin(2*100),1,
R*cos(105),R*sin(2*105),1,
R*cos(110),R*sin(2*110),1,
R*cos(115),R*sin(2*115),1,
R*cos(120),R*sin(2*120),1,
R*cos(125),R*sin(2*125),1,
R*cos(130),R*sin(2*130),1,
R*cos(135),R*sin(2*135),1,
R*cos(140),R*sin(2*140),1,
R*cos(145),R*sin(2*145),1,
R*cos(150),R*sin(2*150),1,
R*cos(155),R*sin(2*155),1,
R*cos(160),R*sin(2*160),1,
R*cos(165),R*sin(2*165),1,
R*cos(170),R*sin(2*170),1,
R*cos(175),R*sin(2*175),1,
R*cos(180),R*sin(2*180),1,
R*cos(185),R*sin(2*185),1,
R*cos(190),R*sin(2*190),1,
R*cos(195),R*sin(2*195),1,
R*cos(200),R*sin(2*200),1,
R*cos(205),R*sin(2*205),1,
R*cos(210),R*sin(2*210),1,
R*cos(215),R*sin(2*215),1,
R*cos(220),R*sin(2*220),1,
R*cos(225),R*sin(2*225),1,
R*cos(230),R*sin(2*230),1,
R*cos(235),R*sin(2*235),1,
R*cos(240),R*sin(2*240),1,
R*cos(245),R*sin(2*245),1,
R*cos(250),R*sin(2*250),1,
R*cos(255),R*sin(2*255),1,
R*cos(260),R*sin(2*260),1,
R*cos(265),R*sin(2*265),1,
R*cos(270),R*sin(2*270),1
It works, but what a horrible polygon definition! It’s enormous! As it turns out, you can store all of these values (R*cos(210), R*sin(2*210), 1, and so on) on the stack, then use the values on the stack to create the polygon. In our example, we will store all of the values onto the stack using a loop. !Cylinder Break Line R = a/2 !Put the curve x, y values on the stack for phi = 0 to 270 step 5 put R*cos(phi), R*sin(2*phi), 1 next phi !Create the poly-line poly2_ nsp/3, 1, get(nsp)
62 Chapter 6: Using the Stack - When to Use the Stack This second script uses put, get, nsp and a for – next loop. Not counting the comments or the sub-routine label, the script is only 5 lines long. Compare this short script with our original 57-line monster (to avoid wasting paper I’ve squashed each set of 3 lines together). You will appreciate how useful it is to be able to put to and get from the stack.
6.3 When to Use the Stack The main use for the stack in regular GDL scripting is to insert values into a polygon or polyline definition as we discussed earlier in the chapter. There are a number of other uses for the stack, of which I would like to mention a few. Finding Minimum and Maximum Values When you have a list of unsorted values for which you want to find the minimum or maximum, you can do so using the stack. Like polygon definitions, the max() and min() functions take a list of data. This list can be provided by the data stored on the stack. Transferring Data to and from Macros There are a number of ways that data can be transferred between objects and macros at run-time. One way is to place data on the stack in one object, and retrieve it in another. For example, you could put data on the stack, and then call a macro that retrieves the data at the head of its script. Similarly, the results of a calculation in a macro can be passed back to the main object on the stack. Transferring Data to and from Sub-Routines Within a script, scoped sub-routines will use different variables to those used by the main script. Before calling the subroutine, it is usually necessary to initialize some of the sub-routine variables. One way to do this without breaking the flow of the main script is to put the initial values onto the stack, and let the sub-routine initialize its own variables using the values it finds on the stack. Inserting Data into a Values List Sometimes when setting up a values list, it would be tedious to hard-script the values. Provided the values are numeric, you can first put them onto the stack, and then insert them into the values command.
6.4 Clearing the Stack A word of caution is in order here. Take great care when using the put(), get() and use() commands. If you accidentally leave data on the stack, it can cause problems if you later use the stack. One way to avoid this would be to clear the stack before using it. You could write a stack-clearing sub-routine like this: “40 Clear the Stack”: !Input: None
Chapter 6: Using the Stack - Put and Get Data within a Block 63 !Return: None if nsp then trash = max(0, get(nsp)) endif return 9
Before using the stack, you would call the sub-routine (gosub “40 Clear the Stack”) to remove any stray data. I would recommend that you only use a stack clearing routine when absolutely necessary. Provided your scripting style is clear and well-organized, you should never end up with stray data floating around.
6.5 Put and Get Data within a Block Later when we discuss scripting style, one of the recommendations I’ll make will be to break your script into independent blocks. When it comes to using the stack, this recommendation holds true. To avoid errors, and to make your script readable and robust, be sure to put and get data within the same block of script wherever possible. If you put data in one block, and get it in another, the potential for error becomes significantly greater, for two reasons. First, the two blocks of script must occur in the correct order. This is kind of obvious - you can’t get data from the stack before you put it there. Practically, this means you have to be more careful when moving blocks of script. Second, you can’t easily use the stack for any of the blocks of script in between. Say you have three blocks of script – we’ll call them blocks A, B and C. In block A you put data onto the stack. In block C, you get this data. As a result, the data must remain on the stack while block B is running. If you want to use the stack in block B, there is a problem. The stack already contains a bunch of data. To get around this you could temporarily store the data in an array variable, use the stack and then return the original data to the stack. While this is a perfectly legitimate process, it should only be used when absolutely necessary – preferably never. Make it a rule to put and get data within a block, and you will seldom if ever have to use this temporary storage method.
9
In the first edition this sub-routine used a loop. I’ve replaced it with a max( ) command for brevity and to reduce the number of variables.
7
Loops
Loops instruct the processor to repeatedly perform, or iterate, a set of commands until a particular condition is met. When the condition is met, the processor will exit the loop and execute the remaining lines of script. In this chapter, we will look at three different loops – the for-next loop, the repeat-until loop and the while-endwhile loop. Each loop has slightly different characteristics which suits it for different applications. We will also discuss that most dreaded yet surprisingly common error in programming, the infinite loop, and its lesser-known but equally repulsive twin, the practically infinite loop. Understanding these two horrors will help us devise strategies to avoid them.
7.1 The ‘for–next’ Loop The simplest type of loop is the for–next loop. This loop uses a numeric counter variable which automatically increments after each iteration of the loop. The initial value of the counter variable is given, and the variable’s value increments until it reaches a given end value. !Syntax for a ‘for-next’ loop for counter_variable = initial_value to final_value step step_size do_something next counter_variable
You can use a decrementing value for the step size. If the step size is negative, the loop will run until the value of the counter variable becomes less than or equal to the end condition. By default the step size is one, and need not be specified. !Syntax for a ‘for-next’ loop with step size of 1 for counter_variable = initial_value to final_value do_something next counter_variable
It’s common to use variables i, j and k for counter variables. We will see later (in the section that deals with infinite loops) that great care should be taken when choosing counter variables. The start and end conditions are calculated once when the loop is initiated. If these conditions rely on variables which change during an iteration of the loop, they will not be re-calculated. As a result, the for–next loop is suited to cases where the end condition is known and fixed, and where the counter variable always increments with a fixed step size. Try it Yourself: Change the End Condition within the Loop The following loop will terminate after three iterations, despite the fact that the variable n increases with each iteration. !Try to change the end condition within the loop
Chapter 7: Loops - The ‘for–next’ Loop 65 n = 1 for i = 1 to 3*n n = n + 1 print i next i
Try it Yourself: Floor Board Section For sections and details, it can be handy to have a 2D object that draws a row of floor boards as cut in section. These sectional board shapes will be placed side by side in a straight line. The first board will be placed at the object’s origin, and the last board will extend to (A, 0) or slightly beyond. Such an object fits our conditions for using a for–next loop. We have a start point, a fixed end point, and a constant step size (one board width).
Figure 62 – Create a new Floor Board object and
Create a new object, with length parameters boardWidth, boardThick, add parameters to define the board dimensions. tongueLength, tongueThick, gap and chamfer (figure 62). Choose the object’s subtype to be Drawing Symbol > Patch. Set sensible default values for the parameters. !Hotspots hotspot2 0, 0 hotspot2 a, 0 !Place a row of boards for x = 0 to a step boardWidth + gap add2 x, 0 gosub "Tongue & Groove Board" del 1 next x end "Tongue & Groove Board": poly2 14, 7, 0, chamfer, chamfer, 0, boardWidth - chamfer, 0, boardWidth, chamfer, boardWidth, (boardThick - tongueThick)/2, boardWidth + tongueLength, (boardThick - tongueThick)/2,
66 Chapter 7: Loops - The ‘for–next’ Loop boardWidth + tongueLength, (boardThick + tongueThick)/2, boardWidth, (boardThick + tongueThick)/2, boardWidth, boardThick, 0, boardThick, 0, (boardThick + tongueThick)/2 + gap, tongueLength, (boardThick + tongueThick)/2 + gap, tongueLength, (boardThick - tongueThick)/2 - gap, 0, (boardThick - tongueThick)/2 - gap
Figure 63 – Floor boards placed using a for-next loop.
return
The script will place the first board at x = 0. The next board will be placed at x = boardWidth, and x will continue to increment until it is greater or equal to a. Click on the 2D Full View button to run the script (figure 63). Try it Yourself: Capitalize Text In this example we’ll use the for-next loop to perform a text substitution that will capitalize a user input string. Create a new object with a text type parameter with variable inputText. Copy the following script into its 2D Script editing window. !Convert the input text to capitals, and print the result inputText~38 = inputText gosub “38 Convert to Upper Case” print capitalizedText~38 end “38 Convert to Upper Case”: !Input: !
- Text to capitalize = inputText~38
!Return: !
- Capitalized text = capitalizedText~38
!Calculation: upperCaseCharSet~38 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" lowerCaseCharSet~38 = "abcdefghijklmnopqrstuvwxyz" capitalizedText~38 = "" for i~38 = 1 to strlen(inputText~38) newChar~38 = strsub(inputText~38, i~38, 1) charIndex~38 = strstr(lowerCaseCharSet~38, newChar~38) if charIndex~38 then
Chapter 7: Loops - The ‘repeat – until’ and ‘while – endwhile’ Loops 67 newChar~38 = strsub(upperCaseCharSet~38, charIndex~38, 1) endif capitalizedText~38 = capitalizedText~38 + newChar~38 next i~38 return
7.2 The ‘repeat – until’ and ‘while – endwhile’ Loops The beauty of the repeat–until and the while–endwhile loops is that their end conditions are re-calculated at each iteration. This allows the loop to terminate at any time, unlike the for-next loop that chugs on for a set number of iterations. As a result, these loops are useful for any situation where the end condition may change while the loop is running, or where the number of iterations does not rely on an incrementing variable. !Syntax for the ‘repeat – until’ loop repeat do_something until end_condition !Syntax for the ‘while-endwhile’ loop while test_condition do do_something endwhile
The practical difference between these two types of loop is that for the repeat–until loop, the end condition is tested following the iteration, whereas the test condition of the while–endwhile loop is calculated before the iteration. This means that the repeat–until loop will always run for at least one iteration. In many ways, it is a matter of individual choice. The loops are largely interchangeable – in some cases one has a slight advantage over the other. Whichever of the two loops you use, there is a real danger of creating an infinite loop. You must be careful to ensure that the end condition will always be met (or that the test condition will eventually fail). We’ll discuss this in the next section. Try it Yourself: Remove All Blank Spaces from a String In the absence of anything useful, we’ll go ahead and use the repeat – until loop to remove all blank spaces from user input. Create a new object, with a text parameter inputText. Copy the following script into the 2D Script editing window. For the default value, type something meaningful like ‘The quick brown fox jumps over the lazy dog’.
68 Chapter 7: Loops - Infinite Loops !Remove all blank spaces from the inputText variable includesBlankSpace = strstr(inputText, " ") while includesBlankSpace do inputText = strsub(inputText, 1, includesBlankSpace - 1) + strsub(inputText, includesBlankSpace + 1, strlen(inputText) - includesBlankSpace) includesBlankSpace = strstr(inputText, " ")
Figure 64 – The warning message shows that the blank spaces have been removed.
endwhile print inputText
Click on the 2D Full View button to run the script. A warning like the one shown in figure 64 should appear. All of the blank spaces have been removed. To see how the repeat – until loop could have been used instead, replace the original script with that below. !Remove all blank spaces from the inputText variable repeat includesBlankSpace = strstr(inputText, " ") if includesBlankSpace then inputText = strsub(inputText, 1, includesBlankSpace - 1) + strsub(inputText, includesBlankSpace + 1, strlen(inputText) - includesBlankSpace) endif until includesBlankSpace = 0 print inputText
As you can see, we had to add an if–endif statement inside the loop which was not required in the previous script.
7.3 Infinite Loops Every loop terminates when the end condition is met. But if for some reason the end condition is never met (or if the test condition is never broken), a loop will repeat endlessly. This situation is known as an infinite loop, and should be avoided like the plague. The result of an infinite loop from the user’s perspective is that the computer crashes, or ArchiCAD freezes, or some other equally unpleasant fate is doled out. Such results don’t tend to win a programmer much popularity.
Chapter 7: Loops - Infinite Loops 69 I should perhaps warn you that some of the examples below will wreak temporary havoc. If you were working on something important in ArchiCAD, you should save it now before trying any of them. Actually, that’s a general rule of thumb tha I use when programming. If I write a loop, the first thing I do before running the script, is to comment the loop out and save the object (and anything else that needs saving). Then I un-comment the loop and run the script. Hopefully the loop works first time, but there is a fair chance that ArchiCAD will grind to a halt and die. In the worst case scenario, I will have to force quit ArchiCAD and re-start it. But the commented loop is still intact, and I can start to search for the error. Infinite loops can occur in many ways. We’ll look at some of the most common.
7.3.1 Step Size One very common cause for infinite loops is when a step size is assumed to be always positive. If the step size is controlled by a parameter (for example the width of a floor board), there is no guarantee that the user will input a positive value. It is quite possible that the user will enter a zero value for the width of a floor board. In this case the end condition will never be met, no matter how many boards are placed. We must ensure that the user is prevented from entering these unexpected values. This can be done by adding some checking code into the Parameter Script. Whenever an invalid value is entered by the user, the Parameter Script substitutes a valid value. In our earlier example of floor boards, the Parameter Script might read as follows. !Set variables buttonID and modparName to stand in for globals buttonID = 0 modparName = "" isFirst = 0 rrr = Application_Query ("Parameter_Script", "FirstOccasion_in_Progress", isFirst) if isFirst then buttonID = glob_ui_button_ID modparName = glob_modpar_name endif !Check for invalid dimensions if modparName = “boardWidth” then if boardWidth < 0.010 then parameters boardWidth = 0.010 endif endif
This might seem rather fussy. You may think that no sane person would ever enter a zero or negative value for the width
70 Chapter 7: Loops - Infinite Loops of a floor board, and if they did they deserve to be punished. However, we programmers should be more charitable. It is surprisingly easy to mistype data. Then again, a user may misguidedly think that using a negative value will set the boards out from the opposite end, which would actually be quite cool though slightly confusing. Another way that the step size could become negative is if it is the result of a calculation. In this case it may not be so easy to pick up. I find that if an unwanted event can happen, it surely will eventually happen. It is much better to ensure that it can’t.
7.3.2 Call a Sub-Routine If a sub-routine is called from within a loop, you should take care that the sub-routine’s variables do not prevent the end condition from being reached. To illustrate this point, type this simple example into the 2D Script of a new object. !Incrementally set variable i while i is being used as a loop control variable for i = 1 to 5 gosub “Set i” next i end “Set i”: i = 4 return
The variable i is used as the test for the end condition of the loop. As soon as i = 5, the loop will end. But at each iteration, the sub-routine “Set i” is called. This sub-routine sets i to 4. As a result, i will never reach the value 5, and the end condition will never be met. The loop will continue endlessly. This is a direct result of using an unscoped sub-routine, and occurs more often than you would imagine.
7.3.3 End Conditions Even if the step size is finite, it is possible to set end conditions that can never be met. Consider the following loop. !Loop until an unreachable end condition is met i = 0 repeat i = i + 1 until i = -1
Obviously, you could continue to add 1 to i for a very long time before i becomes -1. This type of error is generally easier to spot than some of the others, and is therefore less common. However, it can happen, and you should check for it whenever you set up a loop. Here’s another way to set up an end condition that will never be met – simply increment the wrong variable! !Create an infinite loop by incrementing the wrong variable
Chapter 7: Loops - Infinite Loops 71 i = 1 repeat j = j + 1 until i >= -1
Since i is never incremented, it remains at the initial value 1 and therefore never reaches 100. In this case not so difficult to spot, but try the next example. !Remove all blank spaces from the inputText variable repeat includesBlankSpace = strstr(inputText, " ") if includesBlankSpace then inputText = strsub(inputText, 1, includesBlankSpace - 1) + “ “ + strsub(inputText, includesBlankSpace + 1, strlen(inputText) - includesBlankSpace) endif until includesBlankSpace = 0 print inputText
This looks a lot like the script we used in the previous section, but I’ve made a sneaky little edit that changes it from a well-brought-up little script to a fire-breathing monster. Maybe you can spot the problem quickly enough. But imagine if the loop contained 5 times as many lines of script, or possibly a call to a sub-routine. You can see how easy it would be to inadvertently create an infinite loop.
7.3.4 How to Avoid Infinite Loops Even if you take care when scripting loops, you will occasionally create an infinite one. When this happens, ArchiCAD will effectively freeze, and you will have to force it to quit, losing all your work since you last saved it. Whenever you create a loop that has the slightest chance of being infinite, there are some techniques you can use to protect yourself. Do not Save the Script When you save a GDL script, it is first checked. If it contains an infinite loop, the checking phase will take for ever – literally. So before saving the object, pause and take a moment to consider the options available to protect yourself from losing the last hour or so of work. Comment the Script Before you hit the Save button, select the whole script and click the Comment button. Then save the object. This way, the script will save successfully without being checked. Having saved the object, and any other work you may have open at the time, un-comment it and proceed with the next step.
72 Chapter 7: Loops - Practically Infinite Loops Apply a Temporary Counter to Loops The end condition can be set to exit the loop if the counter reaches a ridiculously large value. Once you are certain that the loop is not infinite, remove the counter and the modified end condition. In the example below, the loop is terminated if the variable counter grows larger than 1000, which is to say that 1000 iterations have been completed. This avoids the infinite loop. Once the issue has been located and fixed, the counter variable can be removed and the end condition simplified. !Remove all blank spaces from the inputText variable counter = 1 repeat counter = counter + 1 includesBlankSpace = strstr(inputText, " ") if includesBlankSpace then inputText = strsub(inputText, 1, includesBlankSpace - 1) + “ “ + strsub(inputText, includesBlankSpace + 1, strlen(inputText) - includesBlankSpace) endif until includesBlankSpace = 0 or counter > 1000 print inputText
Test in the Master Script If you are working in the Parameter Script, you may be able to copy the script into the Master Script to test it. The Parameter Script is notoriously difficult to de-bug. In every other script (except for the Interface Script) you can use the print command to keep you informed of variable values as the script runs. But not the Parameter Script. So if you copy the loop temporarily to another script, and run it in isolation, that may help you to identify the problem.
7.4 Practically Infinite Loops There are some situations in which, while you may have avoided an infinite loop, the time taken to complete the loop is unacceptable. Usually this boils down to either a very large number of iterations, or a slow process being carried out at each iteration. You should be aware that this can happen, and have strategies in place to avoid it or to reduce its effect where it is unavoidable.
7.4.1 Small Step Size A practically infinite loop can occur if the step size is not quite zero. Say, for example, that you are placing floor boards.
Chapter 7: Loops - Practically Infinite Loops 73 Instead of stretching the object across the floor, you decide to type in the total floor width. The floor is 3,000mm wide, so you enter 3,000 into the parameter value. Unfortunately, your finger slips and you type an extra zero, so you have entered 30,000. Even worse, you forgot you were working in meters, not millimeters. The result is that you have requested the object to place enough floor boards to cover 30,000 meters of flooring. If each board is 100mm wide, a total of 300,000 board polygons will be required. This may take several seconds to generate. It is easy to imagine scenarios that result in massive quantities of repeated elements being placed. OK, they are not infinite loops. You may have to wait seconds, minutes, hours or even weeks for the end condition to be met, but it will eventually happen. Often a practically infinite loop can be as annoying as a truly infinite loop, or more so. The problem is that you may know that the processor will eventually reach the end condition, so you have to make the decision to wait or not to wait. At least with an infinite loop you have no choice. It is also more difficult to avoid practically infinite loops. It might just be that someone really has a floor 30 km wide. It’s difficult to imagine, but it could happen. To avoid such situations, you need to set the scope of the object. If the object is a set of floor boards, limit it to floor spans of, say, 100m (around 330 ft). There are very few buildings for which this is too small, and for these cases, a second object could be placed to add the extra boards. Similarly, for the Floor Boards object, limit the width of the boards to be at least 25mm (1 inch). I have never come across a narrower floorboard. These limitations will ensure that the maximum number of boards ever placed by one object is at most 4,000.
7.4.2 Monkey Typists If 1, 000, 000 monkeys are given typewriters, one of them will eventually produce the complete works of Shakespeare. This is a classic example of a practically infinite loop. While this exact situation is unlikely to occur in your GDL scripts, it serves to illustrate how easily, and perhaps unexpectedly, numbers can blow out to become practically infinite. Let’s get our computer to stand in for one of the monkeys. Make a new object called Monkey Typist.gsm. Give it a string parameter goalPhrase with the default value “TO BE OR NOT TO BE”. Add a second parameter with the integer variable maxAttempts. Set the default value of the second parameter to be 10, 000. To make it easier for our monkey, we’ll give her a special typewriter with only 27 keys for the upper-case characters A – Z, and a blank space. We’ll ignore lower case characters, numbers and punctuation marks. To give our simian friend an even better chance at success, we’ll also provide some training. After the training, she will be more likely to type certain characters. To train the monkey, we’ll give her a few common phrases to practice on.
74 Chapter 7: Loops - Practically Infinite Loops Admittedly the phrases are taken from a variety of sources, but they are all recognizably from the English language. Copy this GDL script into the 2D window. !Randomize when Moved add2 -symb_pos_x, -symb_pos_y add2 symb_pos_x, symb_pos_y !Practices Phrases dim practiceStr[] practiceStr[1] = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG" practiceStr[2] = "SHE SELLS SEA SHELLS ON THE SEA SHORE" practiceStr[3] = "IT WAS THE BEST OF TIMES IT WAS THE WORST OF TIMES" practiceStr[4] = "IT NEVER RAINS BUT IT POURS" practiceStr[5] = "DOUBLE DOUBLE TOIL AND TROUBLE FIRE BURN AND CAULDRON BUBBLE" practiceStr[6] = "HOW MANY ROADS MUST A MAN WALK DOWN" practiceStr[7] = "YESTERDAY ALL MY TROUBLES SEEMED SO FAR AWAY" practiceStr[8] = "IF SEVEN MAIDS WITH SEVEN MOPS SWEPT IT FOR HALF A YEAR DO YOU SUPPOSE ” practiceStr[9] = “THE WALRUS SAID THAT THEY COULD GET IT CLEAR" practiceStr[10] = "REMEMBER REMEMBER THE FIFTH OF NOVEMBER GUNPOWDER TREASON AND PLOT" !Get the Available Characters from the Practice Phrases dim character[] nChar = 0 for i = 1 to vardim1(practiceStr) for j = 1 to strlen(practiceStr[i]) nChar = nChar + 1 character[nChar] = strsub(practiceStr[i], j, 1) next j next i !Set the Monkey to Work! goalPhraseLength = strlen(goalPhrase) nAttempts = 0 bestPhraseLength = 0 bestPhrase = "" repeat nAttempts = nAttempts + 1 thisPhrase = "" nCorrect = 0 repeat
Chapter 7: Loops - Practically Infinite Loops 75 nCorrect = nCorrect + 1 k = 1 + int(rnd(nChar - .001)) thisPhrase = thisPhrase + character[k] until not(strstr(goalPhrase, thisPhrase)) or nCorrect = goalPhraseLength if nCorrect > bestPhraseLength then bestPhrase = thisPhrase bestPhraseLength = nCorrect endif until nAttempts = maxAttempts or bestPhrase = goalPhrase !Report the Monkey's Best Effort text2 0, 0, strsub(bestPhrase, 1, strlen(bestPhrase) - 1)
Save the object and place a few instances onto the project plan view window. Each time, it will print a different text onto the floor plan. This text represents the monkey’s best effort after maxAttempts attempts. If your monkey typed a string of 6 or more correct characters within 50, 000 attempts, she did very well and should be rewarded. Typically, a monkey would take around 275 (about 14, 000, 000) attempts to achieve this feat. If you’re prepared to wait a little longer, try extending the number of attempts to say 100, 000 or even 1,000, 000. My ‘monkey’ managed the following pitiful best efforts after several sets of 100, 000 attempts: “OT TO”, “BE OR”, “T TO”. Her crowning glory was the profound “O BE O”, a total of 6 characters in the correct order. You can imagine how many monkeys and how much time would be needed to type Hamlet, King Lear and the rest. We could probably improve our chances with some extra training, maybe looking at common letter combinations, a cunningly designed keyboard (we could position the keys in such a way that commonly associated letters are closer together), or a strictly Shakespearian training regime.
7.4.3 Complex Processes If each loop carries out a complex, massive or otherwise slow process, the effect will be magnified when the process is repeated many times. You may find that parts of a process need not be repeated for each loop. Look for those lines of script that need run only once, and those that must be performed at each iteration. Later we’ll look at ways to optimize processes for speed, but for now we’ll make a mental note of this as a possible issue, and move on.
8
Scripting Style
The style that you use when writing a GDL script is nearly as important as the script itself. When you return to a script to fix an issue six months after you created it, you will want to quickly comprehend the script so as to identify where the error is occurring and how to fix it. When another programmer reads your code, he or she should be able to grasp the basic flow of the script at a glance. Unlike script editors such as Microsoft’s Visual Studio programming suite, or CodeWarrior, the GDL editor uses plain, un-formatted text. As a result, you are free to format your script pretty much any way you like. While everyone has their own personal style, there are some fundamental principles that I believe should be adopted by every programmer. In this chapter, I’ll present the style that I now use consistently. I don’t claim that it is particularly original, or even necessarily the best method. However, it has been tried and proven. It works very, very well, and I wholeheartedly recommend it.
8.1 Readable Code If you were to ask me what I believe to be the single most important thing about a GDL script, I would answer without hesitation. It must be readable. Imagine that you are given the choice of two GDL scripts to de-bug. One script has a number of errors, but is well structured, commented, and generally very easy to follow. The other script has only one bug, but has no comments (or, worse yet, misleading ones), is poorly structured, and uses unrecognizable variables. Which script would you choose to de-bug? The obvious choice is the first script. Due to its structure, you will easily identify where the errors occur. Because of its well-designed variables and comments, you will clearly understand the intent of the original programmer. In contrast, locating the single issue in the second script will be like finding the proverbial needle in a haystack. Having located where the script is failing, you must then untangle the twisted logic of the original programmer (perhaps yourself) to understand what he or she intended (all this without any help from comments), and then try to fix it without disrupting the remainder of the script. Even if an error has been fixed in one place, there may well be multiple instances of the same error scattered throughout a poorly structured script. I have frequently experienced both situations. Generally, in the second case I find it is quicker to scrap the old script entirely, and start over. This not only allows me to fix the current issues, but is also an investment in the future. When another programmer wants to make an enhancement to the script in a year’s time, she will have a much easier time of it.
Chapter 8: Scripting Style - Readable Code 77 And it’s not all about bug fixing and making enhancements. There will be times when you want to copy part of a script for use in another object. In cases like this, it’s nice to be able to quickly locate the part you want to copy. I would strongly encourage you to be kind to yourself and to your colleagues, and take every effort to make your script read like a book. While it will take longer to write the initial script, you will save future time and effort.
8.1.1 Comment your Scripts Comments are crucial to the readability of a GDL script. They have absolutely no effect when the script is run, but without them the script is next to useless. To create a comment in your script, type an exclamation mark ‘!’ followed by the text for your comment. Everything following the exclamation mark is ignored when the script is run. Comments can occur at the end of a line of code, but I rarely use them in this way. To me, a comment should be used like a title or sub-title in a book. It explains what the following code is setting out to achieve, and (if necessary) how it will go about achieving this.
8.1.2 Use Meaningful Variables When deciding on a name for a variable, take time out to choose a name that means something. Choose names like cupboardWidth or curtainMaterial as opposed to W or cMat. Anyone can understand the meaning of the first two names, but the alternatives could mean just about anything. Use full words rather than abbreviations. This makes it far easier to remember the name of a variable. If you use abbreviations, you will never be certain that you have used the correct abbreviation, or whether in a particular case you may have used the full word. For example, should you use bdWidth, bdWid, boardWdth, brdWdth or bdWd for the width of a floor board? Keep it simple and use boardWidth – this avoids any confusion. That being said, I do consistently use a set of abbreviations for words that occur commonly throughout my scripts. In particular, I use the abbreviation Mat for material, btm for bottom (which occurs almost as frequently as top), bkgd for background, n for number, R for radius, i, j, k for counters, s, t for ‘time’, and tol for tolerance. There may be one or two others, but these are the examples that spring to mind. It is OK to use a few abbreviations, but you must be consistent.
8.1.3 Use White Space Apart from separating GDL key words, white space (tabs, spaces and empty lines) is ignored by the GDL interpreter. As a result, you are free to use white space wherever you like. White space can be used to break up your script into visual blocks. Indents can help identify the start and end of loops, and which ‘if’s and ‘endif’s belong together. Don’t be shy of using white space. It has no effect on the performance of the scripts, and, when used well, can make your scripts more readable.
78 Chapter 8: Scripting Style - Readable Code Include blank lines between blocks of script, and indent lines that follow a comment or a loop control. This visually breaks the script into stand-alone blocks. Also use white space: •
Around the equals operator: L = 2*pi*R
rather than L=2*pi*R
•
To separate terms in expressions: nBands = int(2 + (a - 2*bandOffset - bandWidth)/bandSpacing)
rather than nBands = int(2+(a-2*bandOffset-bandWidth)/bandSpacing)
•
Following commas: hotspot 0, 0, zzyzx
rather than hotspot 0,0,zzyzx
Example – A Tale of Two Scripts To illustrate the difference between well-formatted and poorly-formatted scripts, I’ll use a sample of script taken from the Barrel Bridge object. A number of constants are calculated in the Master Script, as they are used in both the 2D Script and the 3D Script. Below are two versions of the 3D script. The first version uses abbreviated variable names, no white space, and meaningless variable names. The second version is formatted as described above. Both will work, but I think you will agree that the second version is much easier to follow. While you may think that the unformatted version looks a bit extreme, believe me I have seen worse! Note that the formatted version makes use of a sub-routine – while we haven’t looked at sub-routines yet, they offer another way of adding structure to a script. The unformatted version: toler .001 pen gs_cont_pen hotspot 0,0,0 hotspot a,0,0 hotspot 0,b,0 hotspot a,b,0 hotspot 0,0,zzyzx hotspot a,0,zzyzx
Chapter 8: Scripting Style - Readable Code 79 hotspot 0,b,zzyzx hotspot a,b,zzyzx material TM prism 4,BD,0,0,BT,0,BT,b,0,b addz zzyzx-BD prism 4,BD,0,0,BT,0,BT,b,0,b del 1 addx a-BT prism 4,BD,0,0,BT,0,BT,b,0,b addz zzyzx-BD prism 4,BD,0,0,BT,0,BT,b,0,b del 2 n=int(L/(BdW+BdG)) G=L-n*BdW BdG=G/n BdS=BdW+BdG add 0,b/2,zzyzx/2 for xi=0 to L step BdS qi=(xi/R)*(180/pi) rotx qi addz -R prism 4,BdT,0,-BdW/2,a,-BdW/2,a,BdW/2,0,BdW/2 del 2 next xi del 1 material SM add 0,b/2,BD SS=(a-2*SO-SW)/(nS-1) for i=1 to nS xform 0,0,1,SO+(i-1)*SS,1,0,0,0,0,1,0,0 prism_ 3,SW,0,zzyzx/2-BD,979,zzyzx/2-BD+ST,360,4079,zzyzx/2-BD,360,4079 del 1 next i del 1
The formatted version: !Curve definition to reduce polygon number
80 Chapter 8: Scripting Style - Readable Code toler .001 !View pen pen gs_cont_pen !Hotspots on the bounding box hotspot 0, 0, 0 hotspot a, 0, 0 hotspot 0, b, 0 hotspot a, b, 0 hotspot 0, 0, zzyzx hotspot a, 0, zzyzx hotspot 0, b, zzyzx hotspot a, b, zzyzx !Beams material timberMat !Beams at the near end gosub "Beam" addz zzyzx - beamDepth gosub "Beam" del 1 !Beams at the far end addx a - beamThick gosub "Beam" addz zzyzx - beamDepth gosub "Beam" del 2 !Boards material timberMat nBoards = int(bandLength/(boardWidth + boardGap)) totalGap = bandLength - nBoards*boardWidth boardGap = totalGap/nBoards boardSpacing = boardWidth + boardGap add 0, b/2, zzyzx/2 for xi = 0 to bandLength step boardSpacing qi = (xi/R)*(180/pi) rotx qi addz -R
Chapter 8: Scripting Style - Readable Code 81 !Board prism 4, boardThick, 0, -boardWidth/2, a, -boardWidth/2, a, boardWidth/2, 0, boardWidth/2 del 2 next xi del 1 !Steel Bands material bandMat add 0, b/2, beamDepth bandSpacing = (a - 2*bandOffs - bandWidth)/(nBands - 1) for i = 1 to nBands xform 0, 0, 1, bandOffs + (i - 1)*bandSpacing, 1, 0, 0, 0, 0, 1, 0, 0 prism_ 3, bandWidth, 0, zzyzx/2 - beamDepth, 979, zzyzx/2 - beamDepth + bandThick, 360, 4079, zzyzx/2 - beamDepth, 360, 4079 del 1 next i del 1 end "Beam": prism 4, beamDepth, 0, 0, beamThick, 0, beamThick, b, 0, b return
Note that in this example (as in all other scripts given in the GDL Handbook) I’ve been somewhat miserly with white space so as to save paper in the printed version. Normally I would include a line break before each non-indented comment.
82 Chapter 8: Scripting Style - Structured Scripting
8.2 Structured Scripting Every GDL script you write (other than the Master Script as we’ll see later) should be formatted as illustrated in figure 65. It should begin with a header. A number of script blocks should constitute the body of the main script, which is terminated by the command end. Following the main script, any number of sub-routines may be included.
8.2.1 Include a Header At the top of a GDL script, it is a good idea to include a header – a block of script that lists any indexed variables used in the script, and any calculations that are global to the script. In chapter 22 Quality Control, we will see that the process of declaring a dimensioned variable is relatively inefficient. To avoid any loss of performance, its good practice to limit the number of times variables are dimensioned. Because sub-routines are not scoped, you can (perhaps ‘should’ would be more accurate) also declare any dimensioned Figure 65 – Scripts should be written as a series of well-defined variables used in sub-routines in the header. blocks. Common functions should
Another common use for a header is to perform calculations that may be used be included as sub-routines that throughout the remainder of the script. In some cases, this could be done in the Master are listed under the main script. Script (which is in effect a general header for all the scripts). However, for calculations that relate to one script only, it is somewhat of an over-kill to use the Master Script.
8.2.2 Use Script Blocks Consider three scenarios that occur quite frequently when scripting GDL objects. First, think of the number of times you need to alter a GDL script. Unless you are some kind of programming machine, it is very unlikely that you will create a perfect object at the first attempt. At some stage in its life, every object you make will need to be re-opened for editing. In some cases, the script might need to be extended or improved. In other cases an issue might need to be fixed. Next, think of a situation in which you are de-bugging a complex 3D Script. Some parts of the model just don’t look right, but why, and how to fix them? One very useful technique when de-bugging a script is to simplify it. In many cases, the easiest way to simplify a script is to comment out large portions of it so you can concentrate on a specific part. Then again, you might want to change the order in which certain parts of the script are run. You want to copy one part of the script, and move it up or down to provide a more
Chapter 8: Scripting Style - Structured Scripting 83 logical or intuitive structure. You might even want to grab some lines of script and use them to create a sub-routine. In each of the above scenarios, it makes a lot of sense to write your script in self-contained blocks. By ‘self-contained’, I mean that each block can be moved or removed without causing the rest of the script to go haywire. As far as possible, remove any inter-dependence between the blocks. One way to achieve this is to make each block into a sub-routine. I would recommend this approach if the same block of script is to be used two or more times within the script, or if the block is rather large (i.e. it contains more than say 30 lines of code). For blocks of code that are used only once, and are relatively short, I would recommend they are written in-line. The reason I write script blocks in-line is that I am paranoid about scoping sub-routines (see Force Scope in Sub-Routines). The main script has its own scope, and the blocks of script contained within it share this scope. If you were to take the approach of writing all script blocks as sub-routines, you would have to choose between scoping each of multiple extra sub-routines (which would get tedious), or not scoping sub-routines (which could lead to serious errors).
8.2.3 Use Sub-routines Whenever you need to use a block of script more than once in a script, make it into a sub-routine. That way, if you have to make a correction, you need only do it once. It is poor practice to copy and paste blocks of identical script to different places in a single GDL script.
8.2.4 Force Scope in Sub-routines As mentioned in chapter 3 Variables, GDL scripts have no concept of scope. A variable defined in a sub-routine is global to the script. This can lead to problems if the variable is also used in the main script or in another sub-routine. To avoid conflicts, make it a habit to give each sub-routine a unique ID (unique, that is, in the context of the script – it doesn’t have to be globally unique). Each variable of the sub-routine is suffixed with the same unique ID. This way, you can be certain that each sub-routine uses a totally unique set of variables. I use a simple system for my unique IDs. I append an integer (say _1, _7 or _92) to the variable name. In a few cases it is more efficient to let a sub-routine act directly on main script variables. This is totally OK, as long as you are aware of what you are doing. A typical example of where you should allow a sub-routine to act directly on main script variables is when you are inserting or deleting items from an array. It would be inefficient to create a copy of all the main script arrays for each of these sub-routines. Rather, let the sub-routine act directly on the main variable array.
8.2.5 The Master Script The Master Script is a global header that attaches to all of the other GDL scripts. Use it to perform any calculations that apply to two or more scripts. Use the global variable glob_script_type to perform these calculations only when they are actually required. If a calculation is needed only by the 2D Script and the 3D Script, it would be inefficient to perform it also at the start of the Parameter Script or the Interface Script.
84 Chapter 8: Scripting Style - Managing Transformations
8.3 Managing Transformations When we start modeling objects using GDL scripts, you’ll learn about GDL transformations. The idea behind transformations is that you can move to a new point in space, rotate about an axis, and stretch an axis system prior to drawing some 2D or 3D elements. Once you are done placing elements in that axis system, you can delete the transformations to return to a previous axis system (perhaps the object’s local co-ordinate system). One of the practical implications of block-wise coding is that each block should contain its own set of transformations. These transformations should be deleted at the end of the block. When using transformations, there is a danger that they will build up over the course of the script. While this is not really a problem from the point of view of the GDL interpreter, it can make for some rather unreadable code. Eventually, you will lose track of the accumulated transformations, and you won’t know which way is up, or what direction you are facing. When faced with a situation like this, I always end up going back to the first line of the code and re-scripting in blocks. Working with un-blocked code is like trying to work with a tangled ball of many lengths of string. Every time you want a length, you have to search for its end, and then work your way painstakingly through the knots and loops. If you don’t straighten out a script it remains tangled, so next time you want to work with it you have the same head-ache all over again. Blocked code, on the other hand, is much easier to work with. As each block is self-contained, you can be confident that the only transformations it uses are those listed within the block itself. There might be a little un-tangling to do, but it won’t be more than two or three loops and possibly a small knot. The other great thing about using self-contained blocks of code is that you can pick a block up and move it without affecting the remaining script. This would not work at all if the block relied on an accumulation of previous transformations. You would move the block of code, and not only would the 2D or 3D elements placed by the block itself be misplaced or wrongly oriented, but so would all the elements in the following code.
8.4 Legible vs. Efficient When scripting, you should aim to achieve both readability and efficiency. On the one hand, the script must be clear and readable. Another programmer should be able to open your script, and easily understand the way you have structured it. On the other hand, the script must be efficient when it runs. Nobody wants to use software that slows them down. Usually a well-structured and clearly commented script is also efficient. Comments and white space do not affect the script’s run-time efficiency at all. Similarly, whether you use long or short variable names, makes no difference to the speed of a script’s execution. And because well-written scripts are easier to de-bug, chances are they will contain fewer bugs in the first place – and bugs can be extremely inefficient creatures.
Chapter 8: Scripting Style - Legible vs. Efficient 85 There are times, however, when these two goals can become mutually exclusive. We will see in the next chapter that macros can make for a very readable and well-ordered script, but can lead to inefficiency. On the other hand, sub-routines can be relatively fast, but result in long scripts. The fact that sub-routines must be manually scoped also results in a less readable script. In the final analysis, there will always be situations in which you as a programmer must make decisions about whether to lean more towards efficiency or legibility. I would strongly recommend that you err on the side of legibility except where this has a noticeable effect on speed. Situations in which I would recommend sacrificing readability for the sake of a more efficient script would be: •
Use a sub-routine rather than calling a macro for routine calculations.
•
Design macros that can carry out multiple tasks at a single call.
•
Scope variables in sub-routines to avoid accidentally repeating variable names.
•
Take time to make frequently called macros and sub-routines as efficient as possible.
•
Avoid unnecessary re-calculation by storing the results of calculations.
•
Declare array variables in a header rather than each time they are required.
9
Macros and Sub-Routines
You will find that certain functions are used repeatedly within a given script, while others are commonly used by multiple objects. In such cases it would be great to be able to write a function in one place, and call it from different locations. This would not only simplify the GDL scripts, but would also make it easier to maintain the function. If an enhancement is made to the function (a smarter algorithm might be implemented to improve processing time, or a bug might be fixed), this can be made in one place, rather than having to search through all the scripts that make use of that function. In GDL-speak, the term macro refers to an object that is never placed in a project 10, but is instead used by another object to perform a certain task. A macro might, for example, create a component of the object. (A macro used by a window might be designed to create a glass panel.) A macro can also be used to perform complex calculations and return the results to the main object. Macros can also be used to store and retrieve data from external files, sort and search data, etc. A sub-routine is a block of code designed to perform a specific task. It is generally included after the main part of the script, and can be called from within the main script or from another sub-routine in the same script. The same subroutine can be called from any number of points within a script. We will also see how to share a sub-routine among all the scripts of a particular object. Macros and sub-routines are two ways of achieving a similar result. Both contain blocks of GDL script that can be called from the main program. In this chapter, I’ll explain how to create and use both sub-routines and macros. We’ll consider the benefits of each, and when to use them.
9.1 Macros Let’s look first at how to create and use macros, and then consider the benefits and drawbacks of using them.
9.1.1 Creating a Macro Creating a macro is identical to creating a regular object. Use Figure 66 – Creaing a macro is similar to creating a regular the file dialog to create a new GDL object, add parameters, object. The main difference is that the Placeable checkbox should be set to Off.
10
In some cases, macros are themselves placeable. A cabinet object might use a macro to create a door panel. However, the door panel itself may be placed as an object in its own right. This is relatively unusual, however, and for the sake of this chapter we will assume that macros are not generally placeable.
Chapter 9: Macros and Sub-Routines - Macros 87 and choose an appropriate sub-type (figure 66). Then add GDL scripts to make the macro do something useful. Normally, macros are not intended to be placed directly into a project, so you should set the Placeable flag to zero.
9.1.2 Running a Macro Use the call command to invoke a macro from any point in the script of the main object. !Call a Macro call macro_name parameters parameter_variable_1 = parameter_value_1, parameter_variable_2 = parameter_value_2, parameter_variable_3 = parameter_value_3, . . . parameter_variable_n = parameter_value_n
The macro_name is a string constant for the macro name (not including the .gsm extension). The parameter_variable_s are string constants matching the variables of any of the macro’s parameter values you want to initialize before running the macro. The parameter_value_s are the initial values to which you want to set the specified parameters before running the macro. These values may be any suitable expression – they need not be constants. The macro may include numerous parameters. It is not necessary to set initial values for all of the parameters in the call command. Any parameters not listed will be initialized with their default values. If a macro is called from the 3D Script of an object, the Figure 67 – Create a macro with parameters test1 and test2. macro’s 3D Script will run. Similarly, a macro call from the 2D or Parameters script of an object will cause the 2D or Parameters script of the macro to run. The macro’s Master Script will run before any of its other scripts. Try it Yourself: Create a Macro To see how this works, create a macro named Test Macro with text parameters test1 and test2. Give these parameters default settings (figure 67). Copy the following GDL script into the macro’s 3D Script
Figure 68 – This macro reports the current values of its parameters.
88 Chapter 9: Macros and Sub-Routines - Macros editing window. !Report the local variable values print “test1 = “, test1, “\ntest2 = “, test2
Now click on the 3D View button. A warning message (figure 68) will appear reporting the current values of parameters test1 and test2. Figure 69 – The macro reports the new value sent by the calling Next, create a new object and copy the following GDL script object. into its 3D Script editing window. !Call to the macro call “Test Macro” parameters test1 = “New value”
Click on the 3D View button to run the script. Because the new object calls Test Macro, the corresponding script in Test Macro runs as it did before. However, we have set the initial value of parameter test1 to “New value” in the call command. As a result (figure 69) the warning message reports “New value” instead of the macro’s default value for test1.
9.1.3 Using the Keyword ALL
Figure 70 – Add the second parameter to the calling object.
The key word ALL can be used in cases where the parameter variables of the main object and the macro match. !Syntax to call a macro using matching parameter values call macro_name parameters ALL
The initial values of the parameters in the called macro will match the values currently assigned to the corresponding parameter variables in the main object. This can be very convenient, especially if the macro contains a large number of parameters. Let’s say that the default value of a parameter with variable A in the macro is set to 1.000. Within the 3D Script of the main object, the variable A currently has a value of 0.600. When a macro is called using the parameters ALL keyword, the value of A in the macro will be set to 0.600 just as if you had explicitly listed A = 0.600 in the call command. 11
11
At least that’s how it used to work. Since ArchiCAD 14, I’ve been very cautious about using the parameters ALL command, as it now seems to send the values of the parameters as set in the list, rather than the values of the parameter variables currently in use by the script. It seems safer to list each parameter explicitly.
Chapter 9: Macros and Sub-Routines - Macros 89 Any parameters in the macro that are not included in the main object will take their default values. Try it Yourself: Use the Keyword ‘ALL’ In our previous example, add the parameter test1 to the main object, and give it a default value. Now the main object and the macro have a matching parameters that Figure 71 – The macro now reports the calling object’s values for both can be set using the keyword ALL. Change the main object’s 3D Script to the following.
parameters.
test1 = “New value set in the main object’s script.” call “Test Macro” parameters ALL
Click on the 3D View button to run the script. You will see that when the main object called the macro, it passed the current value of test1 to the macro as its initial value.
9.1.4 Mixing ALL with Listed Parameters You can even mix the keyword ALL with a list of explicit parameters. In this case the syntax to use is: !Syntax to call a macro with mixed ALL keyword and explicit values call macro_name parameters ALL parameter_variable_1 = parameter-_value_1, parameter_variable_2 = parameter_value_2, parameter_variable_3 = parameter_value_3, . . . parameter_variable_n = parameter_value_n
Don’t add a comma after the keyword ALL, just start listing the parameters. In cases where a matching parameter is also listed explicitly, the explicit values will be used in preference to the values set by the ALL keyword.
9.1.5 Returned Variables The macro can return values to the main object. This can be useful if the macro is designed to perform a calculation, and return the result to the main object. To return values, use the GDL command end in the macro, then list the values to return. end returned_value_1, returned_value_2,
90 Chapter 9: Macros and Sub-Routines - Macros returned_value_3, . . . returned value_n
In the main object, add the keyword returned_parameters to the end of the call command, and list the variables to take the returned values. call macro_name parameters parameter_var1 = parameter_val1, parameter_var2 = parameter_val2, parameter_var3 = parameter_val3, . . . parameter_varn = parameter_valn, returned_parameters returned_variable_1, returned_variable_2, returned_variable_3, . . . returned_variable_m
The values returned by the macro are assigned to the listed variables. If one of the listed variables is an array, it takes all the remaining returned values. Note that arrays are returned as a vector of values. To return a 2-dimensional array, include it in the end statement, and collect it in the returned_parameters list as a vector. If you also return the dimensions of the array, you can reconstruct it from the vector.
9.1.6 Designate a Task for the Macro One technique that I use frequently is to include a string-type parameter with variable “task” (you could use “method” or “operation”) to a macro. This variable is used to control the action that the macro performs on any given call. For example, a macro designed to perform polygon operations could add two polygons, subtract one polygon from another, or find the intersection of two polygons. The macro would then return the result of the operation to the main (calling) object. The parameters of this macro might include two polygon definitions and a string parameter operation that tells the macro which of the operations you want to perform. The Master Script of the macro might then be structured as follows: !Carry out an operation requested by the calling object gosub operation !Return the results to the calling object gosub “Return Result” end
Chapter 9: Macros and Sub-Routines - Macros 91 “Polygon Addition”: ... script to add the input polygons ... return “Polygon Subtraction”: ... script to subtract the input polygons ... return “Polygon Intersection”: ... script to intersect the input polygons ... return “Return Result”: ... script to return the result to the main object ... return
On calling the macro, the main script would explicitly set the value of the operation parameters to define which of the operations the macro should perform. If you were to make the “task” parameter a vector, you could get the macro to perform multiple operations in a single call.
9.1.7 Returning Complex Data to the Main Object When a macro is called, it does not always return data to the main object. Sometimes it simply creates a 3D structure, or places some 2D components. Often, though, you will call a macro to perform a complex calculation. In such a case you may want the main script to use the results of the calculation. The data must be returned. Using Returned Parameters We have already seen that the end and returned_parameters keywords can be used to return data and get returned data from a macro. This method can be used to directly return multiple single-valued variables (including string values), or even a one-dimensional array of values to the main object. Returning multiple arrays of values, or returning a two-dimensional array to the main script, cannot be done directly. Instead you must return an ordered list of data that includes the size of each array 12. For example, when you call the Polygon Operations macro, the returned data will include: •
The number of resulting polygons (a single-valued variable nResults in the macro)
•
The number of edges in each polygon (a one-dimensional array variable nEdges[] in the macro since the result of a
12
In fact you can return a 2-dimensional array provided it is of fixed size. If the number of rows and columns is variable, the above statement holds true.
92 Chapter 9: Macros and Sub-Routines - Macros polygon operation may include multiple polygons) •
The x, y co-ordinates that define the start point of each edge (two two-dimensional array variables (resultX[][] and resultY[][] in the macro)
All of this data can be returned in one shot, but it requires a bit of processing for the main object to get the data successfully. In the main script we will declare a returnedData[] variable before calling the macro, and use this as the variable to which all returned data is assigned. !Get returned data in an array dim returnedData[] call “Polygon Processor” parameters inputN = inputN, inputX = inputX, inputY = inputY, operation = “Subtract”, returned_parameters returnedData
In our macro, we could use the end command: !Return data as an array end nResults, nEdges, resultX, resultY
On the face of it this seems perfectly fine. But let’s see what would happen in practice. Let’s say that the macro calculates two resulting polygons, one with 3 edges and one with 4 edges. The first three bits of data returned to the main object are then (2, 3, 4). Now the resultX and resultY arrays each have two rows, one for each polygon. If we look at the number of edges in the two polygons, we can see that the second polygon has more edges than the first. Because the arrays are rectangular, each row has the same number of columns. The number of columns in the array is thus 4, the number of edges in the second polygon. For the example, we will choose the values as tabled below. resultN 3 4
resultX 0 1 1 2 3 3
? 2
resultY 0 0 1 0 0 1
? 1
As you can see, the first row of each array contains an unused column marked ?. Because this value has not been explicitly set, it defaults to zero. The full set of data returned to the main object is thus (2, 3, 4, 0, 1, 1, 0, 2, 3, 3, 2, 0, 0, 1, 0, 0, 0, 1, 1). Obviously this would be different for different input values. It is up to the main object to collect and make sense of this data. The first thing the main object would need to do would be to get the number of returned polygons. !Retrieve the returned data k = 0
Chapter 9: Macros and Sub-Routines - Macros 93 k = k + 1:nResults = returnedData[k] for i = 1 to nResults k = k + 1:resultN[i] = returnedData[k] next i
So far, so good. We know the number of resulting polygons and the number of edges in each. It would be nice if we could now run through the remaining data and assign it to the co-ordinates of each edge. However, because of the way we returned the data, we must first calculate the number of columns in the returned arrays. !Find the number of columns in the returned (rectangular) arrays nColumns = 0 for i = 1 to nResults nColumns = max(nColumns, resultN[i]) next i
Finally, we can assign the remaining values of the returnedData[] array to the x, y co-ordinates of each edge. !Now retrieve the returned data for i = 1 to nResults for j = 1 to nColumns k = k + 1:resultX[i][j] = returnedData[k] next j next i for i = 1 to nResults for j = 1 to nColumns k = k + 1:resultY[i][j] = returnedData[k] next j next i
Even then we can’t be sure that we will get the correct result. Depending on how the macro constructs its result polygons, they may contain extra rows or columns, which would result in the wrong values being assigned. A much better approach is to return only the data that will be used, excluding unused rows or columns of arrays. By deliberately constructing the smallest required data set in the macro, data collection by the main object is much easier, and the whole process is more likely to work consistently. In the main object, the script can be simplified to: !Main Object ... call “Polygon Processor” parameters ... ...
94 Chapter 9: Macros and Sub-Routines - Macros k = 0 k = k + 1:nResults = returnedData[k] for i = 1 to nResults k = k + 1:resultN[i] = returnedData[k] next i for i = 1 to nResults for j = 1 to resultN[i] k = k + 1:resultX[i][j] = returned Data[k] k = k + 1:resultY[i][j] = returned Data[k] next j next i ...
The macro script becomes a little more involved. We will declare a returnedData[] array, populate this array, and return it to the main script. !Macro ... dim returnedData[] k = 0 k = k + 1:returnedData[k] = nResults for i = 1 to nResults k = k + 1:returned Data[k] = resultN[i] next i for i = 1 to nResults for j = 1 to resultN[i] k = k + 1:returnedData[k] = resultX[i][j] k = k + 1:returnedData[k] = resultY[i][j] next j next i ...
In this example, the values returned by the macro are (2, 3, 4, 0, 0, 1, 0, 1, 1, 2, 0, 3, 0, 3, 1, 3, 2). No extra zeros were returned. Although the macro becomes more complex, we end up with a better solution. This solution is superior because: •
The complexity is hidden in the macro – the main objects that use the macro can be simplified.
•
The returned data set is free of extraneous data – all the unused array elements were removed by the macro’s script
Chapter 9: Macros and Sub-Routines - Macros 95 before the data was returned. This means: o
The main object can process the data more efficiently.
o
The main object does not have to calculate the size of the arrays before it re-constructs them.
o
The main object has a better chance of interpreting the data correctly.
Using the Stack Numerical data may also be returned on the stack. This avoids having to declare an array for the returned data, but you must take care to collect all data from the stack. Using the stack requires less scripting than returning parameters, and can result in a tidier looking script Our polygon example would look like this: !Main Object dim resultN[], resultX[][], resultY[][] nResults = 0 resultN[1] = 0 resultX[1][1] = 0 resultY[1][1] = 0 ... call “Polygon Processor” parameters ... ... nResults = get(1) resultN = get(nResults) for i = 1 to nResults resultX[i] = get(resultN[i]) resultY[i] = get(resultN[i]) next i ...
The associated macro would return the results as follows: !Macro ... put nResults for i = 1 to nResults put resultN[i]
96 Chapter 9: Macros and Sub-Routines - Sub-Routines next i for i = 1 to nResults for j = 1 to resultN[i] put resultX[i][j] next j for j = 1 to resultN[i] put resultY[i][j] next j next i ...
It’s easy to accidentally leave data on the stack, and this can cause trouble if you later try to use the stack for a different purpose. Whenever you use the stack, you should collect all the data at the earliest opportunity to avoid errors.
9.1.8 Macros and the Parameter Script There are two reasons why you might call a macro from the Parameter Script. Either you want to define a values list for a parameter, or you want to perform some calculations. Defining lists of values for a parameter can be done directly in the macro, provided the main object and the macro share a given parameter. You can also perform calculations in the macro’s Parameter Script. The only problem comes when you decide to use the macro’s script to store the result of a calculation as a parameter value. While this is possible, it doesn’t always work that well in practice. It’s usually better to return the data to the main object, and to allow the main object to store the results in its own parameter values. I include this word of caution here because the Parameter Script is an odd beast, and can be very difficult to debug. When you use the parameters command to set values, the parameter’s list values are changed but the value of the variable within the script is unchanged. To avoid confusion, you should make it a habit to first set the script variable value, then store the result in the parameter. Of course, when you set a value of a local variable in a macro, the corresponding variable in the script of the main object remains unchanged. You should return the value to the main script, then use the main script to store the value in the parameter. This way you can ensure that the script variable and the parameter value remain synchronized.
9.2 Sub-Routines Sub-routines are script blocks placed at the end of a GDL script, following the end command. They can be invoked any number of times from the main script.
Chapter 9: Macros and Sub-Routines - Sub-Routines 97
9.2.1 Defining a Sub-Routine The first part of a sub-routine is its label. The label can be either a number, or a text string enclosed in quotation marks. The label must be followed by a colon. After the label comes the body of the sub-routine. This is where you write the GDL instructions that make the sub-routine work. The final part of a sub-routine is the GDL command return. This instructs the interpreter to return to the line of code following the gosub command that called the sub-routine. For example, the sub-routine “1 Calculate Angle” 13 might be written as: “1 Calculate Angle”: !Input: dx~1, dy~1 !Return: Q~1 Q~1 = atn(dy~1/dx~1) return
Figure 72 – Sub-routines should
It can be helpful to write an explanatory comment directly following the label. The be listed after the main script. comment contains the names of the input variables that the sub-routine acts on, and the name and meaning of any variable that it will return for use by the main (calling) script.
In fact the variables are not really returned, as they are global to the whole script. But there are certain sub-routine variables that can be ignored, and others that are actually useful to the main script. If the sub-routine itself calls another sub-routine, this can be listed also. The comment will help when you come back in a month or so to re-use the sub-routine. You can clearly see what input values are required, in what format they should be given, what values will be returned, and whether this sub-routine relies on other sub-routines. Note that sub-routines should generally be listed after the main script. The reason for this is that the return at the end of the sub-routine is actually a command to return to the next line of the script following the call to the sub-routine. If the sub-routine is included before the end of the main script, it will run every time the interpreter gets to that line. When the interpreter gets to the return command, it will not know where to return to, as no gosub command was used to activate the sub-routine. A variation of this rule is reserved for sub-routines that are used by multiple scripts within the same library part. In this case, the common sub-routines can be included in the Master Script, and can be isolated from the main script by leapfrogging them via a ‘goto’ command. This technique is described in more detail below.
13
Actually, “1 Calculate Angle” is somewhat more complex than this, but we will discuss this later.
98 Chapter 9: Macros and Sub-Routines - Re-Usable Sub-Routines
9.2.2 Running a Sub-Routine The first step in running a sub-routine is to initialize the input and return variables. For example, the “1 Calculate Angle” sub-routine uses two input variables dx~1 and dy~1 to calculate an returned angle Q~1. Before you run the sub-routine, you should first set values for dx~1 and dy~1. An initial value for Q~1 can be set in the header. Having initialized input and return variable values, you can call the sub-routine using the GDL command gosub, followed by the name of the sub-routine. !Call a sub-routine gosub sub_routine_name
If the sub-routine is used to perform a calculation, the final step might be to assign the result of the calculation to a local variable. For example, the script fragment below loops through all of the edges of a polygon. For each edge, it calculates the change in x and y, then calls the “1 Calculate Angle” sub-routine to obtain the angle edgeAngle[] of that edge. The angle is returned in the variable Q~1, so this value is transferred to the array variable edgeAngle[]. !Initialize Variables used in Sub-routines Q~1 = 0 ... !Calculate the Angle of each Polygon Edge for i = 1 to nEdges nxt = i%nEdges + 1 dx~1 = vertX[nxt] – vertX[i] dy~1 = vertY[nxt] – vertY[i] gosub “1 Calculate Angle” edgeAngle[i] = Q~1 next i ... end “1 Calculate Angle”: ... return
9.3 Re-Usable Sub-Routines Sub-routines are great, but as we’ll shortly see, they do have some problems. While these problems can be solved to a
Chapter 9: Macros and Sub-Routines - Re-Usable Sub-Routines 99 certain extent, the solution is not pretty. It’s not a perfect world, and GDL is not a perfect language. Our challenge then is to find a good solution rather than a perfect one. To decide whether a solution is good, we need a definition of goodness. For the moment, we’ll loosely define a good solution to be one that always works, is re-usable, and is efficient at run-time.
9.3.1 Un-Scoped Variables The variables used in sub-routines are global to the script in which they are defined. This is a nuisance, as it means you can accidentally use a variable name within a sub-routine that is already in use by the main script. This can cause real problems. For example, imagine that you are calling a sub-routine from within a repeat – until loop that uses a counter variable i. Now if the sub-routine contains a for – next loop, it can be tempting to again use i as the counter variable. Unfortunately, because the variable i is global to the GDL script, this will also set the value of i in the main script, resulting in unexpected outcomes and quite possibly an infinite loop. To see this in action, make a new object, and copy the following script into its 3D Script editing window. !Test the problem of un-scoped variables in sub-routines i = 0 repeat i = i + 1 gosub “A Dangerous Sub-Routine” print “i now is “, i until i = 100 end “A Dangerous Sub-Routine”: !This sub-routine doesn’t do anything, but illustrates the point for i = 1 to 10 !Do nothing in particular next i return
Click on the 3D View button to run the 3D Script, and check the value of i as it is printed after every gosub. The main program evidently expects that i will increment from 1 to 100. But every time the sub-routine is run, i is re-set to 10. Thus the main loop can never terminate, and the script will run forever. Not very productive! While the example is trivial, it does illustrate the point. If we change the counter variable in the sub-routine to j, the problem will vanish. However, this is not a good solution in general, as it just sweeps the problem under the carpet. As soon as you use j in the main script, the problem re-surfaces.
100 Chapter 9: Macros and Sub-Routines - Re-Usable Sub-Routines
9.3.2 Variable Names in Sub-Routines As we saw in the previous section, variables used in sub-routines pose a special problem. Because the scope of the variables is global to each GDL script, there is a real danger in using the same variable names in the main program as in the subroutines. To avoid this problem, I would recommend that each sub-routine is assigned a locally unique ID (that is, an ID that is unique to the script in which it occurs). The ID can be added to the end of all variables that should be local to the subroutine. For example, a loop counter variable might be named i in the main program. When a loop is used locally in a sub-routine with ID ‘8’, the local counter variable would be named i8. I like to separate the ID from the variable using an underscore _ character, so my variable would read i_8. Try is Yourself: Use a Sub-Routine to set a Variable Used by the Main Script To see how this works, create a new object, and type the following GDL script into the 3D Script editing window: !Main Program for i = 1 to 5 print “The main program has set i to “, i gosub “1 SubRoutine Name” next i end “1 SubRoutine Name”: for i = 1 to 3 next i print “The sub-routine has set i to “, i return
Click on the 3D View button to run the script. You will see how the sub-routine sets the value of the variable in the main program.
Figure 73 – The sub-routine sets the value of a variable used by the main script.
As you can see, the main script and the sub-routine are fighting over who gets to set the value of i. After a few iterations, click on the Stop button to stop the script. Now change the sub-routine to the following: “1 SubRoutine Name”: for i_1 = 1 to 3
Chapter 9: Macros and Sub-Routines - Re-Usable Sub-Routines 101 next i_1 print “The value of i is still “, i return
Run the 3D Script again to observe how the variables have now been effectively scoped.
9.3.3 Global Re-Usability If you find that you need the same functionality in more than one object, you should consider promoting its status to that of a re-usable sub-routine. Each re-usable sub-routine should have a globally unique ID that is prefixed to its name and appended to each variable that it uses. An opening comment should clearly identify its purpose, what input it requires, and in what form. To distinguish variables of globally re-usable sub-routines from variables of sub-routines that are meaningful only within a specific object, I use the tilde character ~ in place of the underscore character _ to separate the ID from the variable name. Thus a variable i~29 is used for a globally re-usable sub-routine with ID 29, while i_29 is used for an internal subroutine. In the remainder of this book I’ll use the term re-usable sub-routine to mean a sub-routine that is globally re-usable. Such a sub-routine can be copied and pasted directly into any given script without having to re-name variables. There is however a residual problem. Having pasted a sub-routine into hundreds of scripts it is a bit of a mission to change it. At any time it is possible that you will find an improvement that makes the sub-routine perform better. The improvement would then need to be pasted into all those scripts – a daunting prospect. Chances are you will miss a few, and over time you will end up with several different versions of the same sub-routine. We’ll see later that by adhering to some fairly straightforward strategies its possible to manage this problem. To avoid the problem as far as possible, painstaking care should always be used when creating re-usable sub-routines. They should be scripted in such a way that they are efficient, well-commented, bug-free and precisely scoped. They should then be thoroughly tested by inclusion in a few objects, and any bugs fixed, before they are accepted for general use.
9.3.4 Use the Stack to Initialize Variables Having manually scoped the variables in a sub-routine, the input variables must be explicitly initiated each time the subroutine is called. In our example sub-routine 1 Calculate Angle, this is not a big deal, as the sub-routine takes only two input variables dx~1 and dy~1. However, a sub-routine may take a large number of input variables. If such a sub-routine is called from several points in the main script, it can be a bit tedious initializing all of the input variables each time. To alleviate this problem you might want to consider using the stack to initialize the variables. At the start of the sub-routine, add code that will get values from the stack and use them to initialize the input variables. This code will be run each time the sub-routine is called. In the main script, rather than explicitly initializing the subroutine input variables before the gosub command, put the values on the stack instead.
102 Chapter 9: Macros and Sub-Routines - Re-Usable Sub-Routines To see this in action, create a new object, and copy the following script into its 3D Script editing window. !Use the stack to initialize variables for a call to a sub-routine getValues = 1 put a, b gosub “1 Calculate Angle” print Q~1 end “1 Calculate Angle”: !Input: dx~1, dy~1 if getValues then dx~1 = get(1) dy~1 = get(1) getValues = 0 endif !Returned: Q~1 Q~1 = atn(dy~1/dx~1) return
You can see that the sub-routine is a bit larger than before, but the important gain is that it is now easier to call. Just put a couple of values on the stack, set the getValues variable to 1, and invoke the sub-routine using the gosub command. This is much tidier if you have twenty gosubs to the same macro through your main script. It’s also convenient for subroutines that have a large number of input values. Using the stack to send values to a sub-routine is very tidy, but it’s potentially a recipe for disaster. The stack should always be treated with respect, and used with care. If you choose to use this technique, you should be careful to set a flag to let the sub-routine know whether there are values on the stack for use by that sub-routine. The sub-routine should re-set the flag back to zero as soon as it has got the values. In the example above, the variable getValues is used to flag to the subroutine that it should take its values from the stack. You should also be careful to put the values onto the stack in the correct order.
9.3.5 Variations of Sub-Routines Sometimes it will be necessary to create one or more variation of a sub-routine. Each variation might take slightly different input data, or return different data. For example, one sub-routine might find the intersection of two lines. A variation of this sub-routine might also return data on whether the intersection point splits one or other of the input line segments. For efficiency, the variation you choose in any application should perform one task well, rather than caculating data that is not required by the main script. Each variation must be considered as a separate sub-routine with its own
Chapter 9: Macros and Sub-Routines - Which to Use? 103 unique ID. There is of course a balance between the number of variations and the efficiency of the code, and it can be sufficient to set flags to indicate whether or not to perform certain calculations that may only be required in some instances. An example of this can be found in the sub-routine 37 Distance Between Two Lines discussed in chapter 18 Linear Algebra Applications.
9.3.6 Managing Sub-Routines To manage re-usable sub-routines, you could store each sub-routine in a specially designed database. This approach has many advantages, as you can store diagrams and explanatory comments along with the sub-routine. You can also store data about which objects and macros use the sub-routine. Alternatively, each sub-routine can be stored in text files (external to your library). When you need a sub-routine, simply open the text file and copy-paste the sub-routine into your GDL script. On rare occasions, you will find a better solution to an old problem. This might lead to a new sub-routine that will effectively replace an old one. Rather than editing all of the scripts in all the objects that used the old sub-routine, introduce it carefully into each object as you come to update it in your regular maintenance cycle, testing the object carefully to ensure the change in sub-routine has not introduced any bugs.
9.4 Which to Use? Macros and sub-routines perform similar roles. Each has its strengths and weaknesses. In this section I will suggest some guidelines for choosing which approach to take in any given situation.
9.4.1 Strengths and Weaknesses of Macros Macros provide a tidy, well-organized structure to GDL programming. It is possible to make each macro responsible for a well-defined task. Each macro is like a building block. One macro can call another. For example, a macro that calculates the intersection of two polygons might call a second macro that finds the intersection point of two lines. This leads to the concept of a pyramid of macros. Macros at the bottom of the pyramid perform very simple tasks. At this foundational level, one macro might calculate an angle, another calculates the intersection of two lines, and a third sorts a group of data. Every time you want to perform one of these basic tasks, you call the macro. Macros on the second level perform more complex tasks by calling a number of the macros below them. Then there are higher level macros again, that use macros from both of the two supporting levels. Any variable defined within a macro script is scoped to that macro. This means that the same variable names can be used within a macro as are used in the main object that calls it. While the values of the macro variables may change when the macro runs, the values of the variables in the main object will remain unchanged. Because they are scoped, and can be called from any other object or macro, macros are inherently re-usable. This feature of macros is extremely useful. Imagine that you create a macro that performs a useful calculation, and returns the result to
104 Chapter 9: Macros and Sub-Routines - Which to Use? the main object. Any time you want to perform this calculation, all you need to do is to call the macro. When using macros the only drawbacks are: 1.
Macros can cause a slight drop-off in performance, causing the script to run a little slower. In practice you should be careful about how many macro calls an object uses, especially when macros are stacked.
2.
Libraries that share common macros can pose problems. The common macros need to be loaded as a separate library, and this is not always understood by end-users.
9.4.2 Strengths and Weaknesses of Sub-Routines Sub-routines present a flat programming structure that detracts from script readability, and presents some challenges in management. If you want to use the same functionality in two objects, you must copy the sub-routine from one to the other. Because variables in sub-routines are not scoped, you must ensure that variables used in a sub-routine are unique to that sub-routine. Using sub-routines tends to result in long GDL scripts. Without great care, this can lead to scripts that are difficult to read. On the other hand, using a sub-routine is more efficient than calling a macro to do the same task. Because the sub-routine is contained within the object, the object is largely self-contained requiring no external common macros.
9.4.3 Choosing between Macros and Sub-Routines As discussed above, macros and sub-routines each have their strengths and weaknesses. Macros provide a well-organized structure, making for easier management, while calls to sub-routines are performed relatively quickly and result in more self-contained library parts. In other words, macros are great for coding, whereas sub-routines are better for the end users. To decide whether to use a macro or a sub-routine in a particular case, I tend to apply the following guidelines: 1.
Use sub-routines to perform simple processes, and macros for more complex tasks.
2.
Use sub-routines for processes unique to a given object, and macros for common processes used by multiple objects.
3.
Get maximum benefit from each macro call.
4.
Maintain a managed database of re-usable sub-routines.
This section outlines how to put these principles into practice. Use Sub-Routines for Simpler Processes Use a sub-routine rather than a macro wherever it is practical to do so. You must be very careful to manually scope the
Chapter 9: Macros and Sub-Routines - Which to Use? 105 sub-routines. As we’ve discussed, sub-routines can be managed effectively so as to be re-usable. These managed subroutines are each assigned a unique ID, forcing their variables to be scoped. Get the Maximum Benefit from Each Macro Call One way that we can use macro calls more sparingly is to squeeze the maximum benefit out of each call. Rather than calling a macro that performs a single, simple task, we can design macros that can perform a family of related tasks on similar data, and that can even perform multiple tasks at each call. Use a Flat Macro Structure Rather than stacking macros, include all necessary sub-routines within each macro. Macros should be stacked only when two objects share a component that requires calculation. The component macro will then call the calculation macro. Use Multi-Tasking Macros Where appropriate, each macro should be designed to carry out multiple instructions based on multiple input data. To illustrate this point, let’s say we are making a window. The window’s basic shape is defined by a polygon. To form the main frame, trims, etc. this polygon is expanded or contracted. It is then split to allow for the mullions and transoms. The resulting set of polygons is further processed to form sash frames, glass, glazing bars and panels. Each of these polygon processes could be achieved by a separate call to a polygon macro. Alternatively, several processes could be achieved simultaneously by a single macro call. One call can perform a series of tasks. For example, a single call to a Polygon Processor macro can offset an initial polygon to allow for the main frame, find the intersection of the resulting polygons with polygons that define a set of panels, and return the results to the main object. Avoid Unnecessary Processing Another way to streamline GDL scripts is to call macros only when they are needed. Harking back to our window example, there is no need to generate all the polygons whenever the 3D Script is run. The polygon data could be calculated once, and the results stored in parameters of the object. The polygons need only be re-calculated if the shape changes. Maintain a Managed Database of Re-usable Sub-Routines Many of the sub-routines you write will be used by multiple objects and macros. An example of this would be a subroutine that calculates an angle, or one that calculates the intersection point of two lines. I have used these sub-routines in probably hundreds of objects. You could simply write the sub-routine, and, when necessary, copy and paste it into each object that uses it. While this is a valid approach, it does raise some management considerations. First, it is important that each sub-routine has a unique ID. You should at least maintain a list of the sub-routines and their ID, so as not to end up with a duplicate ID. Second, if you find a bug in a sub-routine, or make an improvement to it, after it has been pasted into a number of different objects, you may want to update all or some of the objects that use the sub-routine. It is helpful to keep a record of each object that uses the sub-routine.
106 Chapter 9: Macros and Sub-Routines - Which to Use? One solution to these management criteria is to use a database or spreadsheet to record the various data for each subroutine. I currently use a wiki for this purpose. It allows me to: •
Search for sub-routines by keyword
•
Check which objects use the sub-routine, and whether they are up-to-date
•
View an example of how the sub-routine is used by the main script
•
Check for dependencies (any other sub-routines that are used by the current one)
•
View text and graphics that explain how the sub-routine works.
Most importantly, the wiki is a central resource from which I can copy sub-routines and paste them into GDL scripts.
10 The 2D Symbol Before moving on to 3D modeling (the heart and soul of GDL), we’ll pause to take a look at the 2D elements. This might seem a little boring, and if you want to skip to the 3D section, I totally understand. However, you will end up coming back to this section, because, like it or not, for every great 3D object, there is a 2D symbol. A good 2D symbol is essential to complement the quality of the 3D model. While 3D modeling is undoubtedly the most enjoyable part of GDL, the challenges and rewards of creating quality 2D symbols can be almost as inspiring. Entire libraries of intelligent 2D symbols can be produced without touching the 3D Script. The Cadimage Detail Elements tool is one such library. Such 2D objects can help to boost productivity when drawing details and sections, annotating drawings, etc. Certain object subtypes such as labels, zone stamps and door & window markers, have no 3D part. Another argument in favor of presenting the 2D symbol before the 3D model is that it’s inherently easier to work in 2D than it is to work in 3D. Enough excuses then, let’s take a look at the content of this chapter. • We’ll take a look at 2D transformations – how to move a drawing element to a position, rotate it and mirror it. • Then we’ll consider how to draw a symbol directly into the 2D Symbol drawing window. This method can be useful, but has its limitations. • A superior approach which works well for very complex shapes is to drag-and-drop the drawing elements into the 2D Script, then modify them to provide the required control. • We’ll look at using projections of the 3D model. • Finally we’ll see how to use GDL 2D elements to create a more interactive 2D symbol. While this approach takes more effort, the end result is usually far more satisfactory.
10.1 2D Transformations Each 2D element you construct is defined relative to a rectangular axis system or reference frame. The origin, orientation and scale of the axis system can be changed at any time. The 2D Script allows for just three transformations – translation (add2), scale (mul2) and rotation (rot2). These are used to translate, scale and rotate the x-y axis system.
108 Chapter 10: The 2D Symbol - 2D Transformations
10.1.1
Move the Local Origin
Use add2 command to move (translate) the local origin by a given distance along the current x and y axes. !Move the Local Origin add2 origin_x _shift, origin_y_shift
Try it Yourself: Draw a Rectangular Grid To see how this works, consider this 2D Script which places a series of lines to form a grid. Create a new object with the following two length-type parameters with default values 200mm (or 0’-8”) and variables gridSpacingX and gridSpacingY. Type the GDL script below into the object’s 2D Script editing window. !Constants tol = .0001 !Error Handling gridSpacingX = max(gridSpacingX, tol) gridSpacingY = max(gridSpacingY, tol) !Horizontal Lines for y = 0 to b step gridSpacingY add2 0, y line2 0, 0, a, 0 del 1 next y !Vertical Lines for x = 0 to a step gridSpacingX add2 x, 0 line2 0, 0, 0, b del 1 next x
This script uses the add2 command to prompt the GDL interpreter to shift the local origin before drawing each grid line. Click on the 2D Full View button to view the resulting grid. If you have followed the steps outlined above, it should look something like this (figure 74). If you forgot to add Figure 74 – A grid constructed by the parameters, or if you did not set the default values, then your grid will contain a drawing multiple copies of the same large number of closely-spaced lines. Add the parameters and default values, and run line at different origin points. the 2D Script again.
Chapter 10: The 2D Symbol - 2D Transformations 109
10.1.2
Rotate the Axes
Use the rot2 transformation to rotate the x-y co-ordinate system about its origin. The rotation is counter-clockwise. 2D drawing elements placed following a rot2 transformation will be placed relative to the new axis system. rot2 counter_clockwise_rotation_angle
To see how this works, create a new object and copy the following script into the 2D Script editing window. for phi = 0 to 90 step 5 rot2 phi line2 0, 0, 1, 0
Figure 75 – Radial lines drawn using the rot2 command.
del 1 next phi
Click on the 2D Full View button to run this totally useless script. It should result in a set of radial lines, rotated by increments of 5° (figure 75) .
10.1.3
Scale the Axes
The mul2 transformation scales the x and y axes. Subsequent drawing elements will appear stretched in two directions. mul2 x_axis_scale, y_axis_scale
For example, you could draw an ellipse by stretching the axis system by different amounts in two directions, then drawing a circle. Create a new object, and copy the following GDL script into its 2D Script editing window. !Ellipse
Figure 76 – An elipse created using a circle and the mul2 command.
mul2 a/2, b/2 circle2 0, 0, 1 del 1
Click on the 2D Preview button to display the result (figure 76). Try changing the default values of A and B to see the effect of stretching the circle by different amounts.
110 Chapter 10: The 2D Symbol - Attributes
10.2 Attributes The 2D elements make use of pens, fill patterns and line types. You can set these attributes prior to placing the elements. Use the following GDL commands to set the current attributes. !Syntax to set the current attributes pen pen_index
!Set the current pen index
fill fill_index
!Set the current fill pattern by its index
fill “fill_name” line_type line_type_index
!Set the current fill pattern by its name !Set the current line type by its index
line_type “line_type_name” !Set the current line type by its name
If you are using a parameter to control line type or fill pattern, use the index method to set the current value in the 2D Script. The name of a fill or line type is rarely used. Note that when choosing a pen index via a parameter of the object, the user has the option to choose any pen index from 1 to 256. Two extra indices are available for selection, namely 0 and -1. To the end user, a zero-index pen means an invisible pen. Typically this would be selected for the pen of a background fill if the end user desires a transparent background. A pen of index -1 means that the user wants to match the pen to the background colour of the current view. In any GDL script, you should check that the selected pen lies within the range 1 – 256, and handle it differently if this is not the case. In the case that a ‘background pen’ has been selected, the request “Pen_of_Drawing” should be used to substitute the nearest available pen colour. If a transparent pen was selected, the drawing elements (lines, arcs, circles, text, etc.) that would otherwise be drawn using that pen, should not be drawn (i.e. the lines, arcs, etc. are invisible). For fill polygons, a transparent outline would require a modification to the polygon’s mask value (so that the outline of the fill is not drawn), but no check need be done for the fill or background pens. This error-handling is illustrated in the following 2D Script fragment: !Header rrr = request(“Pen_of_Drawing”, “”, backgroundPen) if somethingPen = -1 then somethingPen = backgroundPen endif … !Draw Something transparentOutline = (somethingPen = 1) if not(transparentOutline) then
Chapter 10: The 2D Symbol - Quick-and-Dirty Ways to Produce a 2D Symbol 111 pen somethingPen line2 0, 0, a, a arc2 0, b, a, 270, 360 endif poly2_B 3, 7 – transparentOutline, somethingFillPen, somethingBkgdPen, a, 0, 1, a, a/2, 1, a/2, 0, 1
10.3 Quick-and-Dirty Ways to Produce a 2D Symbol The following paragraphs describe some ways that you can produce a 2D symbol without having to do much work. Generally these methods are not particularly satisfactory. Use of the 2D Symbol drawing window, and the drag-and-drop approach both result in somewhat static, non-parametric symbols. Projecting the 3D model limits the use of pens and fills, and results in a drastic drop-off in performance for all but the most basic 3D forms. The methods described below can, however be useful in some cases: • The projection method is handy when bug-fixing, to check the alignment of the 2D symbol with the 3D model. • The projection method can be used as a last resort if the 2D projection is difficult to calculate. • The drag-and-drop method is useful for very complex symbols that would take too long to script.
10.3.1
The 2D Symbol Drawing Window
Every object has its own 2D Symbol window, into which you can place drawing elements such as lines, arcs, splines and fill polygons. Sometimes the easiest way to script a 2D symbol is to simply draw or copy it into the 2D Symbol window of the object. Fragments Rather than the layers you use when working with ArchiCAD, the drawing window has fragments. Fragments work like layers. You can assign sets of drawing elements to a fragment, hide or show fragments. However, there are exactly 16 fragments per object, and rather than names they are referred to by index 1 to 16.
Figure 77 – Use the 16 buttons under the preview window to show or hide drawing fragments.
Fragments can be shown or hidden before opening the 2D Symbol window. Use the 16 fragment buttons to show and hide fragments (figure 77).
112 Chapter 10: The 2D Symbol - Quick-and-Dirty Ways to Produce a 2D Symbol Assigning elements to fragments is similar to assigning elements to layers. You can select the fragment from a list from the element’s Settings dialog before or after placing the drawing element (figure 78). By default, the lines, arcs and fills will be placed into fragment 1. Groups of selected elements can be assigned to a drawing fragment by selecting them, clicking on the Edit Selection Set Dialog button in the Info Box, and choosing a fragment (figure 79). Constructing a 2D Symbol You can copy drawing elements from an ArchiCAD window (say plan view, detail view, worksheet, etc.) and paste them into the object. Alternatively you can draw the symbol directly into the object’s 2D Symbol window. Figure 78 – Assign each drawing element to one Using Fragments in the 2D Script
of 16 fragments.
If the object has no 2D Script, or if there are no drawing elements in the 2D Script, any drawing elements you have placed in the 2D Symbol window will be used as the object’s 2D symbol. If you want to display selected fragments, use the fragment2 command. fragment2 fragment_to_display, use_current_attributes
Figure 79 – Assign a selection of elements to a
If the use_current_attributes flag is set to zero, the fragment will be fragment. drawn using the pens, fills and line types you used when you initially drew it. As you can imagine, this is not a great idea. As soon as you place the object in a project that uses a different pen palette or has a different set of fills, the 2D symbol will look terrible. If the use_current_attributes flag is set to 1, the current pen, fill and line type will be used when the fragment is drawn. Either way you get very limited control, since for fill polygons you can’t set the background or fill pens different to the outline pens.
To get better control over the pens and fills used in the drawing fragments Figure 80 – A fill polygon & lines taken from a you have to do silly things like make two fragments, one with the fill polygon detail drawing. backgrounds and another that holds the fill polygon outlines. Even this is unsatisfactory and very clumsy.
Chapter 10: The 2D Symbol - Quick-and-Dirty Ways to Produce a 2D Symbol 113 By providing a parameter for each pen, line type and fill used, we can control the appearance of the 2D symbol. Try it Yourself: Use Drawing Fragments to store a Window Frame Detail To get a feel for how the 2D Symbol window might be used, we’ll create a 2D symbol for an aluminum window frame extrusion. The first step is to draw the extrusion in the ArchiCAD 2D window (figure 80). For this example, I used an existing drawing from a manufacturer. Copy and paste the drawing into the library part’s 2D Symbol window (figure 81).
Figure 81 – Paste the drawing into the 2D Symbol window.
At this point the elements will all be assigned to fragment 1. When using drawing fragments, the challenge is to control the individual pens, line types and fills. • Select all lines and arcs, and use the layer selector to assign them to fragment 2 (figure 82). • Select the fill polygon, and assign it to fragment 3. Set the background pen of the fills to pen zero (transparent) (figure 83).
Figure 82 – Assign lines and arcs to fragment 2.
Having separated the drawing into fragments, we can now control the attributes of each fragment. Set the object’s subtype to 2D Symbol > Patch (figure 84). Add three Pen Index parameters to the object, with variables outlinePen, fillPen and fillBkgdPen. Set default values for these parameters. Add a Fill Type parameter with variable fillType, and set its default value. Copy the following script into the object’s 2D Script editing window. !Background Fill Polygon fill "Solid Fill" pen fillBkgdPen fragment2 3, 1 !Fill Polygon (has a transparent background) fill fillType
Figure 83 – Set the background pen to zero.
114 Chapter 10: The 2D Symbol - Quick-and-Dirty Ways to Produce a 2D Symbol pen fillPen fragment2 3, 1 !Outline pen outlinePen fragment2 2, 1
Figure 84 – Choose the Patch subtype for a
Note the use of a “Solid Fill”. In this instance the fill’s name is used to identify it. 2D symbol. This line of script will work only if a fill with this name actually exists. To ensure the fill pattern’s existence, we could use a Master_GDL script to create it. 14 Hotspots can be placed graphically into the 2D Symbol window. Select the hotspot tool, and click on any point where you want a hotspot to appear (figure 85). Click on the 2D Preview button, and try changing the pen and fill settings to see that the object works correctly. The object is now complete. Save it into your library. In this example, we used two fragments to draw the fill. One of the fragments holds the outline lines and arcs. The other fragment holds a fill polygon with no border.
We used the fill polygon fragment twice in our script, to place background and hatching separately. This was necessary as the fragment command does not Figure 85 – Add hotspots at strategic provide individual control over outline, hatching and background pens (figure points. 86). Problems with Drawing Fragments Graphical scripting is a nice way to start. You can make some good-looking objects quickly, with limited control over the appearance. In some cases, where the polygons are very complex and the shapes are fixed, it is actually preferable to use this method (or better yet the drag-and-drop approach discussed next), as it would be very slow to script each polygon. There are, however, some limitations to this method of writing scripts. For one thing, you can’t easily define fill polygons with individual control over outline, fill pens and background pens. It is 14
This approach to ensuring the existence of a fill is basically fool-proof, and avoids any potential error resulting from missing fills. Oddly, however, ArchiCAD users react with some violence to having fills added to their projects. As a result I now resort to a less intrusive approach to ensuring the existence of fills, as described in the chapter on Quality Control.
Chapter 10: The 2D Symbol - Quick-and-Dirty Ways to Produce a 2D Symbol 115 possible to use fragments to achieve this of course, but it is a rather bumbling approach. You need 3 fragments to define a single shape – one for the outline pen, one for the fill background, and one for the fill pen. As there are only 16 fragments in total, this doesn’t give much scope for variations. The type of editing you can apply to a fragment from within the 2D Script is fairly limited. You can stretch, rotate or move individual fragments, but that’s about it. You can’t adjust individual dimensions to alter the shape. As we’ll see later, the real power of 2D GDL scripting is realized when you define the lines, arcs and fills parametrically and within the 2D Script.
10.3.2
Drag-and-Drop into the 2D Script
Rather than drawing directly into the 2D Symbol window, you can draw a 2D symbol within ArchiCAD, then drag-anddrop the drawing elements into an object’s 2D Script. The result will be a list of transformations, attributes (pens and Figure 86 – Because the lines and the fill polygon are placed on fills), and drawing elements (lines, arcs, fill polygons, etc.). different fragments, pens can be controlled separately. This method is preferable to using the drawing fragments, as the script can be edited to provide direct control over the pens and fills used. Before dragging the drawing elements into the 2D Script editing window, add a block of 20-30 blank lines of script to act as a ‘target area’ into which the drawing elements should be dropped.
10.3.3
Projections of the 3D Model
The last ‘quick-and-dirty’ approach we’ll consider for producing a 2D symbol is to use a projection of the 3D model. Use the project2 command in the 2D Script editing window to create a projection of the 3D model. There are a number of variations to this command, the most useful being the following: !Place a projection of the 3D model onto the 2D symbol project2{3} 3, 270, method, parts, background_colour, fill_origin_x, fill_origin_y, fill_direction, parameters parameter_1 = value_1, parameter_2 = value_2, … parameter_n = value_n
116 Chapter 10: The 2D Symbol - Parametric 2D Elements The reasons this variation is so useful, is that you can: • You can set parameter values within the project2 command. This means that you can inform the 3D Script that it is currently making a model to be used as a 2D symbol. This means it can omit parts that will not be visible in plan view. • You can define a background colour for the symbol. • By varying the method and parts values, you can omit or include contour lines. To draw just the contour, with no fill use the following script fragment: !Projected Outline pen contour_pen project2{3} 3, 270, 2, 15, parameters isPlanView = 1
To draw just a fill polygon with no contour, use the following script fragment: !Projected Background Fill fill fill_pattern pen fill_pen project2{3} 3, 270, 3 + 16 + 32, 7, background_pen, 0, 0, 0, parameters isPlanView = 1
10.4 Parametric 2D Elements GDL objects are inherently parametric. The end-user can adjust characteristic values or parameters of the object to change the shape and attributes of the 2D symbol. To make this work, the 2D symbol should really be constructed using drawing elements whose position, orientation, shape, size and attributes can all be controlled within the script. The following pages describe the 2D elements you will use to produce parametric 2D symbols. This chapter presents only the visible/printable elements. For a description of hotspots and hotarcs, see the chapter on creating user interfaces.
10.4.1
2D Line Segments
Probably the most basic 2D element you can use is the line segment. This element is used in most 2D symbols. In the 2D Script, a line segment is defined by its start and end points. Use the command line2 to create the line segment. !Syntax to draw a line segment in 2D line2 initial_x, initial_y, final_x, final_y
Chapter 10: The 2D Symbol - Parametric 2D Elements 117 The resulting line segment joins the start point (initial_x, initial_y) to the end point (final_x, final_y). Try it Yourself: Draw a 2D Line Segment To see how this works, create a new object and copy the following GDL script into the 2D Script window. !Draw a Line Segment line2 0, 0, a, b
Click on the 2D Full View button to display the resulting 2D symbol. You will see a diagonal line starting at the object’s origin (0, 0) and ending at (a, b). If you change the value of either A or B, the line’s end point will change (figure 87). Arrowheads
Figure 87 – Use the line2 command to draw a 2D line
When you draw lines in an ArchiCAD project, you can attach segment. arrowheads to the ends. In GDL it is not possible to create lines with arrowheads attached. The best you can do is to draw fill polygons to mimic the ArchiCAD arrowheads.
10.4.2
2D Circles and Arcs
In this section we’ll discuss how to use the 2D Script to draw circles and circular arcs. The 2D circle element circle2 requires 3 arguments – the (x, y) coordinates of the center point, and the radius (figure 88). circle2 center_x_co-ordinate, center_y_co-ordinate, radius
Each of the arguments can be either a constant, or an expression. For Figure 88 – 2D arcs are defined by their radius and two example, the following 2D Script will draw a circle of radius A angles. centered at co-ordinates (0, B). circle2 0, B, A
The 2D arc element arc2 is identical to the circle2 element, but in addition to the center and radius it also has a start angle and an end angle. The start and end angles are always calculated in a counter-clockwise direction from the horizontal local x-axis. Further, the arc is always drawn so as to travel counter-clockwise from the start angle to the end angle. arc2 center_x_co-ordinate, center_y_co-ordinate,
118 Chapter 10: The 2D Symbol - Parametric 2D Elements radius, initial_angle, final_angle
To see how this works, create a new object, and type the following GDL script into the 2D Script window. !Draw an arc from 0 to 90 arc2 0, 0, 1, 0, 90
The script instructs the GDL interpreter to create an arc, centered at (0, 0), with radius 1m, initial angle 0º and final angle 90º. Click on the 2D Full View button to view the result (figure 89). Now change the script to read: !Draw an arc from 0 to -90 arc2 0, 0, 1, 0, -90
This script tells the GDL interpreter to draw an arc with the same Figure 89 – A 90° arc drawn using the arc2 command. center point and radius as before, starting at 0º and ending at -90º. The problem is that the arc is always drawn counter-clockwise. So in the example, the arc actually travels through 270º, rather than -90º (figure 90). To get the other part of the arc (figure 91), we must swap the start and end angles as follows: !Draw an arc from -90 to 0 arc2 0, 0, 1, -90, 0
This can be a bit frustrating, but as long as you remember that the arc is always drawn in a counter-clockwise direction, you should be OK. Zero or Negative Radius The circle radius should be greater than zero. If the radius is controlled by a parameter, or is the result of a calculation, you should Figure 90 – The arc is always drawn counter-clockwise. build error handling into the object to ensure that the radius remains greater than zero at all times, or else is drawn only if it is greater than zero. Zero Angles Arcs that are scripted to start and end on the same angle can cause problems. Because the start and end angles are real
Chapter 10: The 2D Symbol - Parametric 2D Elements 119 numbers, they cannot be exactly equal – one is always greater than the other, if only by a tiny decimal amount. So the drawn arc will either be a tiny point or almost 360°, depending on whether the start or end angle is greater. I came across this problem when drawing 2D symbols for doors. The door swing is an arc. Normally, the door is shown as being open at 90º, but in some cases a user wanted to show a closed door leaf. Of course, for a closed door, the arc would be 0º, which in some cases resulted in a full circle being drawn. The solution is simple. If the difference in end and start angles is nearly zero, then don’t draw the arc. Of course, it’s not really that easy – in practice we should also calculate the arc length, as that may be quite large even though the angle is small. The following script fragment would form a reasonable test for whether an arc should be drawn: !Always draws the part of the arc that lies between q1 and q2 if abs(q2 – q1) > 1 or abs(R*(q2 – q1)) > tol then
Figure 91 – Change the start and end angles to draw the other part of the arc.
arc2 xC, yC, R, q1, q2 endif
This script performs both checks, and provided that one of them is satisfied it proceeds to draw the arc. The arc is drawn if either it subtends an angle of more than 1º, or if the arc length is greater than tol – a constant that I declared in the Master Script to be 0.01mm. Asymmetric Line Types Since an arc is always drawn in a counter-clockwise direction, the line type will always be drawn to the inside of the arc. I know of no way to flip a non-symmetric line type to the outside of the arc. Try it Yourself: An Arc with Asymmetric Line Type To see what I mean, create a new object with a Line Type parameter with variable lineType. For its default value, choose a nonsymmetric line type (figure 92). Copy the following GDL script into the 2D Script editing window. !Draw a 90 degree arc line_type lineType
Figure 92 – Choose an asymmetric line type for an arc.
120 Chapter 10: The 2D Symbol - Parametric 2D Elements arc2 0, 0, 1, 0, 90
Click on the 2D Full View button to run the 2D Script (figure 93) You will see that the line is drawn to one side of the arc (in the illustration, the line is drawn to the inside of the arc). Try as you might, there is no way to flip the line to the other side. While this doesn’t help you much, it’s worth knowing, otherwise you might waste time (like I did) trying to get the Figure 93 – An asymmetric line type is always drawn on the same line to flip to the other side. One solution to this problem is to approximate the arc with a spline as discussed in a later section.
10.4.3
side of the arc.
Ellipses
There is no GDL command to draw an ellipse directly. Bear in mind, however, that an ellipse is simply a circle that got squashed or stretched. As a result, you can use the circle2 command to draw an ellipse if you first scale the axes. Try it Yourself: Define an Ellipse To see how this works, create a new object and type the Figure 94 – Draw an ellipse as a distorted circle. following GDL script into the 2D Script window. !Draw an ellipse that fits into the !bounding rectangle A x B add2 a/2, b/2
!Move to the ellipse center
mul2 a/2, b/2
!Squash/stretch the circle
circle2 0, 0, 1
!Place the distorted circle
del 2
Click on the 2D Preview button, and adjust the A and B values to see the result (figure 94).
10.4.4
2D Splines
Splines are sets of mathematical curves that run smoothly Figure 95 – This object uses splines to trace vehicle tracking through a set of points (figure 95). Later we will see how to curves. create spline curves mathematically, but for now we are more interested in how to use the GDL commands to draw such
Chapter 10: The 2D Symbol - Parametric 2D Elements 121 curves as part of a 2D symbol. Two types of spline are available - spline2 and spline2A. Splines are used mainly to approximate shapes or to indicate a vague curve through space. It is seldom necessary to use splines to draw a precise 2D shape. One exception to this is using splines to approximate arcs. To define an automatically smoothed spline, use the following script fragment: !Draw a smoothed spline spline2A n, mask, x1, y1, 0, 0, 0, x1, y1, 0, 0, 0, … xn, yn, 0, 0, 0
Set the mask value to 2 for an open curve or 3 for a closed curve. All the ‘zeroes’ are actually useful variables. Check the GDL Reference Manual for more information about what extra controls are available.
10.4.5
Use a Spline to Draw an Arc
Spline can be used to draw an arc with an asymmetric line type (figure 96). As we saw in the section on 2D Circles and Arcs, all arcs are traced in a counter-clockwise direction. As a result, an arc drawn using an asymmetric line type will always draw the line the same way around. There is no way the line can be flipped from the inside of the arc to the outside of the arc. Splines, on the other hand, are traced in whatever direction you choose to specify. If it’s important that the user can flip the line orientation, the trick is to use a spline that follows the path of the arc. Rather than using the regular command for an arc … !Draw an Arc arc2 xC, yC, R, q0, q1
… use the following sub-routine instead. “41 Draw an Arc using a Spline”: !Input: !
- arc center: xC~41, yC~41
!
- radius: R~41
!
- start and end angles: q1~41, q2~41 if getValues then xC~41 = get(1)
Figure 96 – Use splines to create arcs that can take asymmetric line types.
122 Chapter 10: The 2D Symbol - Parametric 2D Elements yC~41 = get(1) R~41 = get(1) Q1~41 = get(1) Q2~41 = get(1) endif !Return: NONE !Draw the Arc if Appropriate dq~41 = q2~41 – q1~41 if abs(dq~41) > 1 or abs(R~41*dq~41) > tol then spline2A 5, 2, xC~41 + R~41*cos(q1~41), yC~41 + R~41*sin(q1~41), 0, 0, 0, xC~41 + R~41*cos(q1~41 + dq~41/100), yC~41 + R~41*sin(q1~41 + dq~41/100), 0, 0, 0, xC~41 + R~41*cos(q1~41 + dq~41/2), yC~41 + R~41*sin(q1~41 + dq~41/2), 0, 0, 0, xC~41 + R~41*cos(q2~41 - dq~41/100), yC~41 + R~41*sin(q2~41 - dq~41/100), 0, 0, 0, xC~41 + R~41*cos(q2~41), yC~41 + R~41*sin(q1~41), 0, 0, 0 endif return
This sub-routine will draw a very close approximation to an arc. You can run the arc in either direction (clockwise or counter-clockwise) by swapping the angle order. This makes it ideal for use with asymmetric line types.
10.4.6
2D Fill Polygons
GDL provides commands to create filled polygons that include a boundary line and fill. The polygons can contain holes. Individual control is available for the visibility of each polygon edge. Pen indices can be specified for outline, fill and fill background. Even the origin and orientation of the fill pattern can be controlled. Fill Polygon Definition 2D polygons are defined by a set of edges which can be either line segments or arcs. The polygon as a whole is defined by all or some of the following data: •
the number of vertices it contains
•
a ‘mask’ which indicates whether to include the boundary contours and/or the fill
•
the pens used for the boundary, fill and background
Chapter 10: The 2D Symbol - Parametric 2D Elements 123 •
fill origin and orientation
•
the (x, y) co-ordinate and status codes for each vertex
Polygon Control Depending on the level of control required, use either the poly2_, poly2_b or the poly2_b{2} command to create filled 2D polygons. Use poly2_ to create outlines of polygons, or poly-lines. The poly2_ definition takes the following arguments: !Syntax for the poly2_ fill polygon poly2_ number_of_vertices, mask_to_control_frame_and_fill, first_vertex_x, first_vertex_y, first_vertex_status, ... last_vertex_x, last_vertex_y, last_vertex_status
Use poly2_B to create filled polygons, where you need to control the foreground and background pens of the fill. The poly2_B definition takes the following arguments: !Syntax for the poly3_B fill polygon poly2_B number_of_vertices, mask_to_control_frame_and_fill, fill_pen, fill_background_pen, first_vertex_x, first_vertex_y, first_vertex_status, ... last_vertex_x, last_vertex_y, last_vertex_status
Use poly2_B{2} if you also want to control the fill origin and orientation. The poly2_B{2} definition takes the following arguments: !Syntax for the poly2_B{2} fill polygon poly2_B{2} number_of_vertices, mask_to_control_frame_and_fill, fill_pen, fill_background_pen, fill_origin_x, fill_origin_y, fill_angle, first_vertex_x, first_vertex_y, first_vertex_status, ... last_vertex_x, last_vertex_y, last_vertex_status
Try it Yourself: A Floor Board Section As an example, let’s create a scripted fill to represent a tongue-and-groove floor board. Create a new object with the following parameters:
124 Chapter 10: The 2D Symbol - Parametric 2D Elements Variable
Type
Name
Default Value
sectionPen
pen
Pen of Cut Edges
3
sectionFill
fill
Fill of Cut Surfaces
(choose a fill)
sectionFillPen
pen
Pen of the Fill Pattern
1
sectionBkgdPen
pen
Pen of the Fill Background
19
boardThick
length
Thickness of the Board
24mm (or 1”)
tongueLength
length
Length of the connecting Tongue
15mm (or ½”)
boardWidth
length
Width of the Board (not including the tongue)
145mm (or 6”)
grooveDepth
length
Depth of the V-Groove
4mm (or 1/8”)
chamfer
length
Chamfer to the Tongue End
1mm (or 1/16”)
gap
length
Gap between adjacent boards
1mm (or 1/16”)
Copy the following script into the 2D Script editing window. !Tongue and Groove Board pen sectionPen fill sectionFill poly2_B 16, 7, sectionFillPen, sectionBkgdPen, 0, boardThick, 1, 0, 2*boardThick/3 + gap, 1, tongueLength, 2*boardThick/3 + gap, 1,
Figure 97 – This floor board section was created using the poly2_B command.
tongueLength, boardThick/3 - gap, 1, 0, boardThick/3 - gap, 1, 0, grooveDepth, 1, grooveDepth, 0, 1, boardWidth - gap - grooveDepth, 0, 1, boardWidth - gap, grooveDepth, 1, boardWidth - gap, boardThick/3, 1, boardWidth - gap + tongueLength - chamfer, boardThick/3, 1, boardWidth - gap + tongueLength, boardThick/3 + chamfer, 1, boardWidth - gap + tongueLength, 2*boardThick/3 - chamfer, 1,
Chapter 10: The 2D Symbol - Parametric 2D Elements 125 boardWidth - gap + tongueLength - chamfer, 2*boardThick/3, 1, boardWidth - gap, 2*boardThick/3, 1, boardWidth - gap, boardThick, 1
Click on the 2D Preview button to see the result (figure 97). Now try changing the values of parameters boardWidth, boardThick, etc. You will see how the single polygon definition can be used to model a variety of floor boards. Number of Edges
Figure 98 – This warning results from a mismatch in the number of edges.
The number of edges can be defined by a constant or a variable. If you use a constant as in the tongue-and-groove floor board example above, it pays to go back and count the number of edges after you have scripted them. It is easy to get the number of edges wrong, and if you do, an error message will result. Try it Yourself: Change the Number of Edges Try changing the number of edges in the floor board polygon to the value 17. Click on the Check Script button, and you will see an error message (figure 98). Mask to Control Contour and Fill Choose a mask value to control the inclusion of boundary contour and fill. To calculate the polygon mask, add the following numbers together: •
1 (to include the boundary contours)
•
2 (to fill the polygon - only works if the polygon is closed)
•
4 (to create a closed polygon)
•
8 – for poly2_B{2}, this status allows you to set a local fill origin
Figure 99 – Set Fill Category in the Fill Settings dialog.
For filled polygons, then, use the mask value 1 + 2 + 4 = 7. For empty polygons use the mask value 1 + 4 = 5, and for open poly-lines use the mask value 1. Mask to Control Fill Category ArchiCAD uses three categories of fill.
126 Chapter 10: The 2D Symbol - Parametric 2D Elements Drafting Fills include those fill polygons drawn directly by the ArchiCAD user when creating details. Cut Fills are those fills that are applied, in section views, to elements cut by the section line. Cut Fills are also used on the 2D symbol of walls and columns, since these are displayed in plan view as if cut at a set height. Cover Fills are used by the floor plan symbol of slabs, meshes and roofs. When placing a fill polygon in ArchiCAD, you can set its category using the Fill Settings dialog (figure 99). You can also set a fill polygon’s category when you use it in a GDL script. So why bother with setting the Fill Category in a GDL script? To answer this question, consider the fact that the general appearance of each set of fills can be controlled independently. Select the Document > Set Model View >Model View Options menu item, and a dialog will appear, providing control over the appearance of drafting, cut and cover fills (figure 100). When creating a scripted fill polygon, you should select the most appropriate category for the polygon. For example, if you are scripting a wall return for a door or window opening, you should set the fill category to Cut Fill. This will ensure that the appearance of the wall return will always match that of the wall’s plan view symbol. If Cut Fills are set to Solid, for example, any wall returns (cavity closers) used by doors and windows should also become solid to match. Similarly, the 2D symbol for wall ends should automatically display as solid fills in order to match the walls. By default, any scripted fill polygon is a drafting fill. To assign the fill polygon to a different category, add the following number to its mask value either 32 – if the fill should be a Cut Fill, or 64 – if the fill should be a Cover Fill. For example, the following fill will be interpreted as a Cut Fill: poly2_ 4, 7 + 32, 0, 0, 1, a, 0, 1,
Figure 100 – Use the Model View Options dialog to control the appearance of different categories of fill for each model view.
Chapter 10: The 2D Symbol - Parametric 2D Elements 127 a, b, 1, 0, b, 1
By adding ‘+ 32’ to the mask, the fill is identified as belonging to the Cut Fill category. Outline, Fill and Background Pens The outline of the filled polygon will take the pen index previously set. In the poly2_ command, the fill pen will match that of the outline, whereas in the poly2_B and poly2_B{2} commands, the fill and background pens are declared as arguments of the command. This makes the poly2_B and poly2_B{2} commands better suited to drawing filled polygons. The index of the fill and background pens may be set to zero (invisible), or to -1 (match project background). If the fill pen is set to either 0 or -1, it will revert to matching the outline pen. It is important to use a non-zero pen for the outline pen. If the user selects the value of the outline pen to be 0 or -1, an error message will occur. You should include an error-handling routine to force the outline pen to take a value between 1 and 255. See the error handling discussed in the section titled Selecting Pens. Try it Yourself: Set Outline, Fill and Background Pens To see how these pens work, create a new object of sub-type Furnishing, and copy the following GDL script into its 2D Script editing window. !Fill Definition including Pens pen gs_cont_pen fill gs_fill_type poly2_b 4, 7, gs_fill_pen, gs_back_pen, 0, 0, 1, a, 0, 1,
Figure 101 – Choose a hatched fill so as to preview both fill and background pens.
a, b, 1, 0, b, 1
Set the default values of gs_cont_pen, gs_fill_type, gs_fill_pen and gs_back_pen in the Parameters list. Choose a hatched fill such as Aluminum so you can preview both the fill and background pens (figure 101). Click on the 2D Full View button to run the script (figure 102). Finally, try choosing pens 0 and -1 for the background and the fill pens. Vertex Co-ordinates and Status Codes Each vertex of the polygon is shared by two edges. One edge enters the vertex (we’ll
Figure 102 – Preview the fill polygon.
128 Chapter 10: The 2D Symbol - Parametric 2D Elements dub this the incident edge) and another edge emerges from it. Each edge is defined by its initial vertex, and a status code that controls the edge visibility and shape. The visibility part of the status code always refers to the emerging edge. Syntax
Meaning
x, y, 0
An invisible edge emerges from this point.
x, y, 1
A visible edge emerges from this point.
x, y, -1
No edge emerges from this point – this vertex marks the end of the polygon
Note that the first point of a polygon cannot include any shape data. It must always have a status code of either 0 or 1. If the last point of a polygon has a status code of -1, its co-ordinates must be identical to those of the first point. We will see later that it is not always strictly necessary to use a -1 status code to mark the end of a polygon. The shape part of the status code may refer to the incident edge or the emerging edge. Every edge is a straight edge unless one of the following values is added to the status code. The most useful status codes are listed below. For a full list of all possible status codes, refer to the GDL Reference Manual. Syntax
Meaning
x, y, 900
The point defines the center of an arc, and is not an edge vertex. The center point applies to all subsequent edges until a new center point is defined.
x, y, 1000
The edge ending at this point is an arc tangential to the previous edge.
x, y, 3000
The incident edge is an arc that uses the most recently set center point.
Illustration
Chapter 10: The 2D Symbol - Parametric 2D Elements 129 0, α, 4000
The edge is an arc through a specified angle about a previously defined center point. Rather than giving the vertex co-ordinates in this case, set the first ordinate to zero and the second to the angle through which the arc turns.
Using these codes, you can include arcs as well as straight edges in the polygon. For example, the status code 3001 is made up of a shape status 3000 and a visibility status 1. The shape status 3000 tells the GDL function to draw the previous edge as an arc. The visibility status 1 tells the GDL function that the next edge is visible. Try it Yourself: Arc by Center and Angle For example, let’s make a fill polygon representing an I-Beam cross section (figure 103). Make a new object with the following parameters. Variable sectionPen sectionFill sectionFillPen sectionBkgdPen flangeWidth flangeThick webDepth webThick rootRadius
Type pen fill pen pen length length length length length
Name Pen of Cut Edges Fill of Cut Surfaces Pen of the Fill Pattern Pen of the Fill Background Flange Width Flange Thickness Web Depth Web Thickness Root Radius
Default Value 3 Aluminium 1 19 125mm (or 5”) 12mm (or ½”) 340mm (or 13½”) 8mm (or 1/3”) 6mm (or ¼”)
Copy the following script into the 2D Script editing window. !Draw an I-Beam using status value 4000 pen sectionPen fill sectionFill poly2_B 20, 7, sectionFillPen, sectionBkgdPen, 0, 0, 1, flangeWidth, 0, 1, flangeWidth, flangeThick, 1, (flangeWidth + webThick)/2 + rootRadius, flangeThick, 1, (flangeWidth + webThick)/2 + rootRadius, flangeThick + rootRadius,900, 0, -90, 4001,
Figure 103 – Steel section defined using status 4000.
130 Chapter 10: The 2D Symbol - Parametric 2D Elements (flangeWidth + webThick)/2, webDepth - flangeThick - rootRadius, 1, (flangeWidth + webThick)/2 + rootRadius, webDepth - flangeThick - rootRadius, 900, 0, -90, 4001, flangeWidth, webDepth - flangeThick, 1, flangeWidth, webDepth, 1, 0, webDepth, 1, 0, webDepth - flangeThick, 1, (flangeWidth - webThick)/2 - rootRadius, webDepth - flangeThick, 1, (flangeWidth - webThick)/2 - rootRadius, webDepth - flangeThick - rootRadius, 900, 0, -90, 4001, (flangeWidth - webThick)/2, flangeThick + rootRadius, 1, (flangeWidth - webThick)/2 - rootRadius, flangeThick + rootRadius, 900, 0, -90, 4001, 0, flangeThick, 1
For each of the four root radii, the center point is first defined (e.g. (flangeWidth - webThick)/2 - rootRadius, flangeThick + rootRadius, 900) then the arc edge is defined by the angle -90º and the status code 4001. Try it Yourself: Tangential Arc to End Point A quicker approach to the I-Beam example is to define the arc edges as tangential arcs to an end point. The arcs start off tangentially to the previous edge, and end at the x, y co-ordinates of the end point. The status code to use is 1001 (figure 104). “I-Beam”: !Create a fill polygon that represents an I-Beam pen sectionPen fill sectionFill poly2_B 16, 7, sectionFillPen, sectionBkgdPen,
Figure 104 – Steel section defined using status 1000.
0, 0, 1, flangeWidth, 0, 1, flangeWidth, flangeThick, 1, (flangeWidth + webThick)/2 + rootRadius, flangeThick, 1, (flangeWidth + webThick)/2, flangeThick + rootRadius, 1001, (flangeWidth + webThick)/2, webDepth - flangeThick - rootRadius, 1, (flangeWidth + webThick)/2 + rootRadius, webDepth - flangeThick, 1001, flangeWidth, webDepth - flangeThick, 1, flangeWidth, webDepth, 1,
Chapter 10: The 2D Symbol - Parametric 2D Elements 131 0, webDepth, 1, 0, webDepth - flangeThick, 1, (flangeWidth-webThick)/2 - rootRadius, webDepth-flangeThick, 1, (flangeWidth-webThick)/2, webDepth-flangeThick-rootRadius, 1001, (flangeWidth - webThick)/2, flangeThick + rootRadius, 1, (flangeWidth - webThick)/2 - rootRadius, flangeThick, 1001, 0, flangeThick, 1 return
In this case, the polygon definition requires fewer points (16 instead of 20), and the center points, radii and angles are calculated automatically. Holes, Sub-Polygons and End Points Whenever you intend to create a hole in a polygon, you need to define a set of sub-polygons. To define the end point of a sub-polygon, repeat the first vertex of the sub-polygon with the status code -1. You cannot add this status code to a shape status to control the shape of the last edge of a sub-polygon. The end point must have the status set to exactly -1, and must be the same as the first point of the sub-polygon. The first sub-polygon you define should contain all the subsequent sub-polygons, which will appear as holes in the first sub-polygon. Try it Yourself: A Fill Polygon containing a Hole For example, let’s make a polygon that represents a hollow rectangular steel section. First, create a new object with the parameters listed below. Variable sectionPen sectionFill sectionFillPen sectionBkgdPen profileWidth profileDepth profileThick profileRadius
Type pen fill pen pen length length length length
Name Pen of Cut Edges Fill of Cut Surfaces Pen of the Fill Pattern Pen of the Fill Background Flange Width Flange Thickness Web Depth Web Thickness
Copy the following GDL script into the 2D Script editing window. “Rectangular Hollow Section”: !Create a fill polygon that represents an RHS steel section pen sectionPen fill sectionFill
Default Value 3 (choose a fill) 1 19 50mm 80mm 3mm 8mm
132 Chapter 10: The 2D Symbol - Parametric 2D Elements poly2_b 20, 7, sectionFillPen, sectionBkgdPen, 7, profileWidth/2, 0, 1, profileWidth – profileRadius, 0, 1, profileWidth, profileRadius, 1001, profileWidth, profileDepth – profileRadius, 1, profileWidth – profileRadius, profileDepth, 1001, profileRadius, profileDepth, 1, 0, profileDepth – profileRadius, 1001, 0, profileRadius, 1, profileRadius, 0, 1001, profileWidth/2, 0, -1 profileWidth/2, profileThick, 1, profileWidth - profileRadius, profileThick, 1, profileWidth – profileThick, profileRadius, 1001, profileWidth – profileThick, profileDepth – profileRadius, 1, profileWidth – profileRadius, profileDepth – profileThick, 1001, profileRadius, profileDepth – profileThick, 1, profileThick, profileDepth – profileRadius, 1001, profileThick, profileRadius, 1, profileRadius, profileThick, 1001, profileWidth/2, profileThick,-1
Define a Full Circle To define a polygon that is a full circle, use the 4001 status, but use the two values to specify the radius and an angle of 360º. For example, the following script will draw a circular fill polygon of radius 0.5m (figure 105). !Create a full circle of radius 0.500 pen gs_cont_pen fill gs_fill_type R = 0.500 poly2_B 2,7, gs_fill_pen, gs_back_pen, 0, 0, 900, R, 360, 4001
10.4.7
Fill Polygons with Control over the Fill Pattern
Figure 105 – A circular fill polygon.
Chapter 10: The 2D Symbol - Drawindex 133 If you need to control the origin and direction of the fill, you should use poly2_B{2}. This command is like poly2_b, but allows you to control the fill handle. If you make the fill handle and orientation parametric, you can even provide dynamic hotspots to allow the user to control these parameters. To define a poly2_B{2} element, use the following GDL script, substituting appropriate values or variables for the arguments. !Syntax for the poly2_B{2} fill polygon poly2_B{2} number_of_vertices, mask_to_control_frame_and_fill, fill_pen, fill_background_pen, fill_origin_x, fill_origin_y, fill_angle, first_vertex_x, first_vertex_y, first_vertex_status, ... last_vertex_x, last_vertex_y, last_vertex_status
To control the fill origin and angle: •
Add the number 8 to the mask_to_control_frame_and_fill value.
•
The values fill_origin_x and fill_origin_y control the co-ordinates of the fill origin.
•
The value fill_angle controls the orientation of the fill.
If the values fill_origin_x, fill_origin_y and fill_angle, are controlled by parameter variables, dynamic hotspots can be provided to control the fill origin and orientation.
10.5 Drawindex Often it is necessary to stack 2D elements. Normally, the order the 2D elements are created determines which ends up on top. The first item to be listed is sent to the back, and the last to be listed ends up on top. Oddly, however, lines and arcs take precedence over fill polygons. If you draw some lines, then place a fill polygon on top, the lines will remain visible. It’s as if the lines sort of float to the top of the stack. Even the borders of fill polygons sometimes take precedence if the fills are not completely enclosed. This can cause some annoying effects, with lines unexpectedly appearing on top of fills instead of being masked by them. Try it Yourself: Experiment with the Drawindex Command To see this for yourself, make a new object with Fill Type and Pen Index parameters fillType, fillPen, fillBkgdPen, fillType2, fillPen2 and fillBkgdPen2. Set some default values for the pens and fills. Copy the following script into its 2D window. !Draw the lines first pen outlinePen line2 0, 0, a, b
134 Chapter 10: The 2D Symbol - Drawindex line2 0, b, a, 0 !Draw a fill polygon with all edges fill fillType R = min(a, b)/4 poly2_b 2, 7, fillPen, fillBkgdPen, 3*a/4, 3*b/4, 901, R, 360, 4001 poly2_b 4, 7, fillPen, fillBkgdPen, 0, 0, 1, a/2, 0, 1, a/2, b/2, 0, 0, b/2, 1 !Draw a fill polygon with all edges fill fillType2 R = min(a, b)/2 poly2_b 2, 7, fillPen2, fillBkgdPen2, a/2, b/2, 901, R, 360, 4001
Click on the 2D Full View button to view the result (figure 106).
Figure 106 – Note how the drawing elements are layered. Lines and arcs always appear on top of fill polygons.
As you can see, the two lines are in front of the fill polygons even though the polygons were placed after the lines. In contrast, the small round fill polygon is drawn behind the large round one, including the outline. But look at the small square polygon! While the fill pattern is obscured by the large round fill polygon, the outlines are sitting on top of it! The drawindex command provides more dependable control. Figure 107 – The drawIndex is
Use this command to force certain elements in front or behind others. It works a bit similar to the Display Order like the bring-to-front or send-backward options in ArchiCAD’s Display Order menu (figure 107). Within a set of elements sharing the same drawing index, elements are stacked as normal. !Set the Drawing Index for the Following Elements
Chapter 10: The 2D Symbol - Drawindex 135 drawindex display_order_index
The value of display_order_index sets the order in which elements are displayed behind and in front of others. Oddly, there are only five values available display_order_index – 10, 20, 30, 40, and 50. Elements placed on drawindex 50 are drawn in front of all other 2D elements. By default, elements are placed on drawindex 10. Add some drawindex commands into the script as follows. !Draw the lines first drawindex 10 ... !Draw a fill polygon with all edges drawindex 20 ... !Draw a fill polygon with all edges drawindex 30 ...
Click on the 2D Full View button to see how the drawindex commands effect the 2D symbol (figure 108). Now the square fill and the lines are masked by the large round fill polygon, and the lines are masked by the small fills too.
Figure 108 – Use the drawIndex command to control which elements are drawn first.
11 The 3D Model This chapter introduces 3D GDL modeling. We’ll take a look at a selection of the available 3D elements. I’ve included only those elements that I have found to be most consistently useful. There are some that I have deliberately omitted (cwall_, bwall_, xwall_, xwall_{2}, beam, croof_, armc, arme, elbow, rect, circle, arc, pyramid, coons, mass). These are elements that I have seldom if ever needed in over a decade of programming. Some have been rendered obsolete by the (relatively new) group operations, while others are just not that useful except perhaps for rather rare, specific applications. The GDL Reference Manual in the ArchiCAD Help menu provides a full list of the available 3D elements.
11.1 General Controls Before we get into the 3D Script, I’d like to briefly introduce one or two general controls that apply to all the 3D elements. We’ll first look at attributes to see how pens, fills and materials are applied to the various elements. Next, I’ll show you how to control which elements cast and receive shadows. Finally, we’ll discuss how to make the 3D elements display as solid bodies, hollow surfaces or wireframe models.
11.1.1
Pens and Materials
Each 3D element is displayed using an outline pen and a material. pen pen_index material material_index
Whenever you set the current material and outline pen, it applies to the following lines of script. In some cases, the materials are included in the element’s definition – these override the current material. Note that when choosing a pen index via a parameter of the object, the user has the option to choose any pen index from 1 to 256. Two extra indices are available for selection, namely 0 and -1. For the 3D model, there seems to be no reason why the end user would select either of these pen indices. If they are selected, I believe it is safe to assume the user has made a mistake. To avoid the occurrence of an error message in such a situation, I would suggest that the pen be set to match that listed in the Floor Plan and Section settings in a header as follows: if pen_index < 1 then pen_index = symb_view_pen endif ...
Chapter 11: The 3D Model - General Controls 137
11.1.2
Line Type
In 3D there are no line types available. Every 3D line is a solid line. If you want to draw a dashed line, you must draw each dash as a separate line. A sub-routine for drawing dashed lines is included in the section on Basic 3D Elements.
11.1.3
Bodies Cut in Section
Figure 109 – Enable the object’s Cut
When a body is cut by a section line, you can control the boundary and fill type of Surface attributes. the cut surfaces. Use the sect_fill command. !Set the attributes for any cut surfaces sect_fill fill_type, background_pen, hatching_pen, outline_pen
The sect_fill command allows you to apply a different hatching to each component of the 3D model. If you don’t use sect_fill, the generic Cut Surfaces settings will be used. Because fills and materials use indices, it is impossible to set default values that will apply universally. The safest approach is to use default materials and fill types that have low indices. For example, for a stainless steel material use index 11, and for timber use index 14. Most people will have materials 11 and 14 set to stainless steel and timber. If you use the sect_attrs command instead, you can also set the line type. sect_attrs fill_type, background_pen, hatching_pen, outline_pen, line_type
11.1.4
Shadows
Shadows look cool but can slow things down. Each polygon casts a shadow that may cover other polygons, so if there are hundreds of thousands of polygons in Figure 110 – Use the shadow command to your model the amount of processing required can quickly mount up. Not every control whether a body casts shadows or object in your model needs to cast a shadow. For example, floor joists are generally receives cast shadows from other bodies. covered by floor boards, which will cast their own shadow, so the joists themselves probably don’t need to bother. You can choose to turn shadow casting on and off
138 Chapter 11: The 3D Model - General Controls for different bodies within the same 3D Script. !Control Shadows for the following Bodies shadow cast_shadow_keyword, take_shadow_keyword
The cast_shadow_keyword can be either off, auto or on. The take_shadow_keyword can be either off or on.
11.1.5
Smoothness Control
Many of the basic 3D forms include curved surfaces. Such surfaces are made up of numerous edges and polygons. To control the number of edges and polygons on a surface, use the resol or toler command (figure 111). !Syntax to control the Smoothness of Curved Surfaces resol number_of_edges_in_a_full_circle toler maximum_deviation_from_true_curve
Figure 111 – The surface of
The first of these commands sets a fixed number of edges to approximate any full circle. For every 3D body is made up from example, if you set the resolution to 36, every full circle would be approximated by 36 edges. planar polygons. If you set the resolution to 6, a full circle would be approximated by 6 edges, forming a regular hexagon. The second command sets a maximum deviation between the true arc and the straight edges used to approximate it. This is quite clever, as larger radii will contain more edges than smaller radii. A thin wire, for example, may only use 3 edges, which is fine since you won’t be examining it with a microscope. To be in even closer control of the number of edges in a full circle, you could first use your own method for calculating how many edges should be used for a given radius, then use the resol command. For example, you could calculate the best number of edges for a given radius based on the edge offset, but you might want to use at least 6 edges say. !Set the Resolution alpha = acs((R – 0.001)/R) nSegments = max(6, int(180/alpha)) resol nSegments
11.1.6
Model Type
Any solid body can be displayed as a surface or wireframe model using the model command. !Syntax to set the Model Type model wire
Figure 112 – Calculate how many line segments are required to approximate an arc of radius R subtending angle α so that the line segments never deviate more than Д from the true curve.
Chapter 11: The 3D Model - 3D Transformations 139 model surface model body
To be fair, I’m not quite sure why you would want to do this. Perhaps the surface option is more useful than the wire option, as you could use it to make a hollow sphere or box which could have some useful application.
11.2 3D Transformations Transformations in 3-space are a natural extension of the transformations of the 2D Script. As in the 2D Script, transformation commands will translate, rotate and scale the current axis system so that you can place 3D elements at different positions and orientations, and squash and stretch them in different directions. In chapter 19 Matrices and Transformations, I’ll show you how to use the xform command. This command allows you to perform multiple transformations in a single line of code, change axes without using rotations, and generally save time and make your scripts more readable.
11.2.1
Translations
You can move the current origin to a new point in space using either the addx, addy, addz or add commands. addx origin_x_shift addy origin_y_shift addz origin_z_shift add origin_x_shift, origin_y_shift, origin_z_shift
In these commands, the origin of the new axis system will move along the current x, y and z axes by origin_x_shift, origin_y_shift and origin_z_shift.
11.2.2
Rotations
The axis system can be rotated about the current x, y or z axis using the rotx, roty or rotz commands. rotx rotation_angle roty rotation_angle rotz rotation_angle
The direction of the axis you choose to rotate about remains unchanged. The other axes rotate about it by the rotation_angle. The direction of rotation is conventional. If you make a ‘thumbs up’ gesture with your right hand, and point your thumb along the direction of the pivot axis, your fingers will indicate the direction of rotation.
11.2.3
Scale the Axes
To scale the axes use the GDL commands mulx, muly, mulz or mul. mulx x_axis_scale_factor
140 Chapter 11: The 3D Model - Basic 3D Elements muly y_axis_scale_factor mulz z_axis_scale_factor mul x_axis_scale_factor, y_axis_scale_factor, z_axis_scale_factor
The x, y and z axes will be scaled by the values of x_axis_scale_factor, y_axis_scale_factor, z_axis_scale_factor. You can use these commands to re-size, stretch or squash 3D elements.
11.3 Basic 3D Elements This section identifies some useful 3D elements that are defined using a fixed number of values. These are quite useful for specific situations, but their shapes do not provide much flexibility and so their use is limited. Often there are other modeling elements that will produce a better result. Of all the basic elements listed in this section, the cylinder is the one that is most commonly used. I guess that is because rigid pipes and rods are quite commonly used in buildings.
11.3.1
Lines
To draw a straight line in the 3D window, use the lin_ element. !Draw a 3D Line Segment lin_ initial_x, initial_y, initial_z,
Figure 113 – Use the lin_ command to draw a 3D line segment.
final_x, final_y, final_z
To see how this works, create a new object, and copy the following script into its 3D Script editing window. !Draw a Line Segment starting at the Origin lin_ 0, 0, 0, a, b, zzyzx
Click on the 3D View button to run the script. You should see a diagonal line starting at the origin and ending at (a, b, zzyzx) (figure 113). While you can choose pens and materials for the 3D elements, you cannot choose line types, even when using the lin_ element. If you want to draw a dashed line, you must use a Figure 114 – Draw a dashed series of solid lines (figure 114). !Draw a Dashed Line Segment getValues = 1 put 0, 0, 0, a, b, zzyzx gosub “7 Dashed Line”
3D line.
Chapter 11: The 3D Model - Basic 3D Elements 141 The following re-usable sub-routine will draw a dashed line of dash length dashLength between two points in space. You could modify the sub-routine to draw different line types, or possibly even a generic line type specified in a similar way to a 2D line type definition. “7 Dashed Line”: !Input: !
- Line end points: (x1~7, y1~7, z1~z) and (x2~7, y2~7, z2~7)
!
- Dash length: dashLength~7 if getValues then x1~7 = get(1): y1~7 = get(1): z1~7 = get(1) x2~7 = get(1): y2~7 = get(1): z2~7 = get(1) dashLength~7 = get(1) getValues = 0 endif
!Return: NONE !Draw a dashed line !Calculate the line length and direction vectors ux~7 = x2~7 - x1~7 uy~7 = y2~7 - y1~7 uz~7 = z2~7 - z1~7 L~7 = sqr(ux~7^2 + uy~7^2 + uz~7^2) ux~7 = ux~7/L~7 uy~7 = uy~7/L~7 uz~7 = uz~7/L~7 !Calculate the length of the first and last dashes nDashes~7 = int(1 + (L~7 + dashLength~7)/(2*dashLength~7)) extraLength~7 = (nDashes~7*(2*dashLength~7) – dashLength~7) – L~7 startLength~7 = dashLength~7 - extraLength~7/2 !Draw the lines add x1~7, y1~7, z1~7 lin_ 0, 0, 0, startLength~7*ux~7, startLength~7*uy~7, startLength~7*uz~7 L1~7 = startLength~7 + dashLength~7 repeat L2~7 = min(L1~7 + dashLength~7, L~7) lin_ L1~7*ux~7, L1~7*uy~7, L1~7*uz~7, L2~7*ux~7, L2~7*uy~7, L2~7*uz~7
142 Chapter 11: The 3D Model - Basic 3D Elements L1~7 = L2~7 + dashLength~7 until L1~7 > L~7 del 1 return
11.3.2
Cuboid
A cuboid can be created using either the block or brick commands. !Syntax to Model a Cuboid block x_dimension, y_dimension, height brick x_dimension, y_dimension, height
The prism or extrude commands could also be used. We ‘ll look at these commands later in the chapter.
Figure 115 – Model a cuboid.
Try it Yourself: Model a Cuboid Create a new object, and copy the following script into the 3D Script editing window. brick a, b, zzyzx
Click on the 3D View button to run the script (figure 115).
11.3.3
Cylinder
To create a cylinder, use the cylind command. The cylinder has a vertical axis (figure 116) !Syntax to Model a Cylinder cylind height, radius
Figure 116 – Model a cylinder.
As with all curved bodies in GDL, while the cylinder looks smooth, it is in fact made up of numerous planar surfaces. Try it Yourself: Model a Cylinder Create a new object and copy the following script into its 3D Script editing window. model wire cylind zzyzx, a/2
Click on the 3D View button to run the script. The model wire will force the cylinder to display its edges as a wireframe model (figure 117). You will see that the cylinder is made Figure 117 – The curved surface up of straight edges and flat polygons. Use resol or toler to control the number of edges of a cylinder is constructed from and polygons. multiple planar surfaces.
Chapter 11: The 3D Model - Basic 3D Elements 143
11.3.4
Cone
The GDL cone element defines a truncated cone. !Syntaz to model a Cone cone center_height, base_radius, top_radius, base_angle, top_angle
Angles are measured from the z-axis about the y-axis (figure 118). Thus an angle of 90° will produce a flat topped cone. As for the cylinder element, use resol or toler to control the number of Figure 118 – Dimensions used bey the GDL cone command. edges and polygons. Try it Yourself: Model a Cone If you want to see the cone in action, create a new object with length-type parameters coneHeight, baseRadius and topRadius, and angle-type parameters baseAngle and topAngle. Set non-zero default values for all the parameters. Copy the following script into the 3D Script editing window. !Model a Cone cone coneHeight, baseRadius, topRadius,
Figure 119 – Model a cone.
baseAngle, topAngle
Click on the 3D View button to run the script (figure 119). Try varying the parameter values to see the effect of changing the angles and radii.
11.3.5
Spheres and Ellipsoids
Use the sphere element to create spheres, spheroids and ellipsoids. !Syntax to model a Sphere sphere sphere_radius
The sphere’s center is anchored at the local origin. If you want to move the sphere to a different location, use add, addx, addy, addz or xform to move the origin before placing the sphere object. Figure 120 – Model a Sphere
144 Chapter 11: The 3D Model - Basic 3D Elements For spheroids and ellipsoids, you will have to scale the axes before placing the sphere element (figure 121). !Spheroid radii a, b and zzyzx mul a, b, zzyzx sphere 1 del 1
Figure 121 – Model a Spheroid.
To create a half-sphere or half-ellipsoid, use the ellips element (figure 122). !Model a Half-Ellipsoid ellips height, radius
Figure 122 – Model a halfEllipsoid.
All curved surfaces are made up of straight edges and planar surfaces, as you can see if you use the model wire statement at the head of the script (figure 123). !Model a Half-Ellipsoid in Wireframe Mode model wire ellips height, radius
Figure 123 – The curved surface is made up of planar polygons.
Chapter 11: The 3D Model - 3D Polygons and Planes 145 Use resol or toler control the number of polygons on the surface, and hence the smoothness of the surface (figure 124). !Model a Chunky Half-Ellipsoid in Wireframe Mode model wire resol 12 ellips height, radius
11.4 3D Polygons and Planes
Figure 124 – Low Resolution.
Just as you can create lines in 3D, you can also create planes. These have no thickness, but have a shape defined by a polygon. You can define a plane using the plane command or the poly_ command. The poly_ command is almost identical to the poly2_ command used in the 2D Script. It draws a polygon in the local x, y plane. The plane command allows you to draw a plane defined by points in 3D. You should be careful that the points really do lie on a single plane.
11.4.1
Poly
For extremely basic forms, in which all the edges are to be visible and straight and there are no holes, use the poly command. The beauty of this command is its simplicity. All you need to define is the number of edges, the thickness of the prism, and the x, y vertices of each edge. poly number_of_points, x1, y1, x2, y2, x3, y3, ... xn, yn
11.4.2
Edge Shape
Most of the remaining elements we will look at in this section use the same shape status values as the poly2_ command. I’ve copied the table here for quick reference.
146 Chapter 11: The 3D Model - 3D Polygons and Planes Syntax
Meaning
x, y, 900
The point defines the center of an arc, and is not an edge vertex. The center point applies to all subsequent edges until a new center point is defined.
x, y, 1000
The edge ending at this point is an arc tangential to the previous edge.
x, y, 3000
The incident edge is an arc that uses the most recently set center point.
0, α, 4000
The edge is an arc through a specified angle about a previously defined center point. Rather than giving the vertex co-ordinates in this case, set the first ordinate to zero and the second to the angle through which the arc turns.
11.4.3
Illustration
Poly_
To model more complex forms, you can use the poly_ command. This command allows you to define a status for each edge, as in the poly2_ command. This means you can include curved edges and holes. poly_ number_of_points, x1, y1, status_code _1, x2, y2, status_code _2, x3, y3, status_code _3, ... xn, yn, status_code _n
Chapter 11: The 3D Model - Prisms, Slabs and Extrusions 147 Each edge of the prism is defined by its start point (or some other significant data) and a status code. The status code is the sum of integer codes that define whether the vertical edge is visible, whether the upper and lower edges are visible. Use the status value 0 for a visible edge and 1 for an invisible edge. For curved edges, add the appropriate shape status values to the status codes.
11.4.4
Plane
The plane_ element is similar to the poly_ element but the polygon may be tilted out of the x-y plane. It is up to you to ensure that all the points lie in the same plane. plane number_of_points, x1, y1, z1, status_code _1, x2, y2, z2, status_code _2, x3, y3, z3, status_code _3, ... xn, yn, zn, status_code _n
11.5 Prisms, Slabs and Extrusions Prisms are 3D forms that add a thickness to a polygon. They occur with surprising frequency in GDL scripts. For example, a prism might be used to model a window frame, components of a cabinet, or a desk top. Anything that is constructed by cutting a shape from a sheet of material that has constant thickness can be modeled using a prism. Depending on the application, there are three useful ways to define a prism – using either the command prism, prism_ or cprism_. I’ve lumped slabs and extrusions into this section, because they are similar to prisms in many ways. Slabs are, in effect, prisms that are tilted at an angle (similar to an ArchiCAD roof). Extrusions take a shape (similar to the polygon surface of a prism), and extrude it in a general direction (whereas the prism always extrudes the polygon perpendicular to the plane of the polygon).
11.5.1
Prism
For extremely basic forms, in which all the edges are to be visible and straight, and there are no holes, use the prism command. The beauty of this command is its simplicity. All you need to define is the number of edges, the thickness of the prism, and the x, y vertices of each edge. !Syntax to model a Prism that contains only Straight Edges prism number_of_points, thickness, x1, y1, x2, y2,
148 Chapter 11: The 3D Model - Prisms, Slabs and Extrusions x3, y3, ... xn, yn
You might use a simple prism to model a book shelf (figure 125). Make a new object, and add length-type parameters with variables plinthHeight = 150mm, toeSpace = 80mm, carcassThick = 18mm, backThick = 12mm, shelfThick = 18mm and shelfSetback = 30mm. Add an integer parameter nShelves = 2. Copy the following GDL script into the 3D Script editing window. !Model a book shelf using the prism command !Plinth prism 4, plinthHeight, carcassThick, 0, a - carcassThick, 0, a - carcassThick, b - toeSpace, carcassThick, b – toeSpace !Left End prism 4, zzyzx, 0, 0, carcassThick, 0, carcassThick, b, 0, b !Right End prism 4, zzyzx, a - carcassThick, 0, a, 0, a, b, a - carcassThick, b !Base addz plinthHeight prism 4, carcassThick, carcassThick, backThick, carcassThick, b, a - carcassThick, b, a - carcassThick, backThick del 1 !Top addz zzyzx - carcassThick
Figure 125 – A book shelf modeled using prisms.
Chapter 11: The 3D Model - Prisms, Slabs and Extrusions 149 prism 4, carcassThick, carcassThick, backThick, carcassThick, b, a - carcassThick, b, a - carcassThick, backThick del 1 !Back addz plinthHeight prism 4, zzyzx - plinthHeight, carcassThick, 0, carcassThick, backThick, a - carcassThick, backThick, a - carcassThick, 0 del 1 !Shelves shelfSpacing = (zzyzx - plinthHeight - 2*carcassThick)/(nShelves + 1) for i = 1 to nShelves addz plinthHeight + baseHeight - shelfThick + i*shelfSpacing prism 4, shelfThick, carcassThick, backThick, carcassThick, b - shelfSetback, a - carcassThick, b - shelfSetback, a - carcassThick, backThick del 1 next i
11.5.2
Prism_
To model more complex forms, you can use the prism_ command. This command allows you to define a status for each edge, as in the poly2_b command. This means you can include curved edges and holes. !Syntax to model a Prism that may contain Curved Edges and Holes prism_ number_of_points, thickness, x1, y1, status_code _1, x2, y2, status_code _2, x3, y3, status_code _3, ... xn, yn, status_code _n
150 Chapter 11: The 3D Model - Prisms, Slabs and Extrusions Each edge of the prism is defined by its start point (or some other significant data) and a status code. The status code is the sum of integer codes that define whether the vertical edge is visible, whether the upper and lower edges are visible, and whether the side surface is visible for that edge. code
meaning
illustration
0
1
Bottom edge is visible
2
Side edge is visible
code
meaning
illustration
4
Top edge is visible
8
Side surface is visible
Some examples of the codes are shown below. code
meaning
1 + 4 + 8 = 13
Top & bottom edges are visible, side surfaces are present.
1+8=9
Bottom edges are visible, side surfaces are present.
2 + 4 + 8 = 14
Only the bottom edges are missing.
1 + 2 + 4 + 8 = 15
All edges are visible, side surfaces are present.
illustration
Chapter 11: The 3D Model - Prisms, Slabs and Extrusions 151 For smooth, curved surfaces, where the edge is visible only if it forms part of the outline of the model , add the special modifier 64 to the edge visibility status. For curved edges, add the appropriate shape status values to the status codes.
11.5.3
Edge Shape
Apart from the prism element, all of the elements we will look at in this section use the same shape status values as the poly2_ command. I’ve included the table here for quick reference. Syntax
Meaning
x, y, 900
The point defines the center of an arc, and is not an edge vertex. The center point applies to all subsequent edges until a new center point is defined.
x, y, 1000
The edge ending at this point is an arc tangential to the previous edge.
x, y, 3000
The incident edge is an arc that uses the most recently set center point.
0, α, 4000
The edge is an arc through a specified angle about a previously defined center point. Rather than giving the vertex co-ordinates in this case, set the first ordinate to zero and the second to the angle through which the arc turns.
11.5.4
Illustration
Cprism_
The cprism_ element is virtually identical to the prism_ element, except that you can set its top, bottom and side
152 Chapter 11: The 3D Model - Prisms, Slabs and Extrusions materials independently. cprism_ top_material, bottom_material, side_material, number_of_points, thickness, x1, y1, status_code _1, x2, y2, status_code _2, x3, y3, status_code _3, ... xn, yn, status_code _n
11.5.5
Cprism_{2}
This bad boy is a relative newcomer to the prism family. Each edge of the prism can have a separate material assigned to it, and also a separate splay angle. cprism_ top_material, bottom_material, side_material, number_of_points, thickness, x1, y1, splay_angle_1, status_code _1, material_1, x2, y2, splay_angle_2, status_code _2 material_2, x3, y3, splay_angle_3, status_code _3, material_3, ... xn, yn, splay_angle_n, status_code _n, material_n
The splay angles are measured from the normal, so an angle of zero will result in something that looks like a regular prism.
11.5.6
Bprism_
The bprism_ element is identical to the cprism_ element, but is bent up out of the x-y plane along the surface of a cylinder. bprism_ top_material, bottom_material, side_material, number_of_points, thickness, radius, x1, y1, status_code _1, x2, y2, status_code _2, x3, y3, status_code _3, ... xn, yn, status_code _n
11.5.7
Slab_
The slab_ element is similar to the prism_ element but the base and top polygons are tilted out of the x-y plane. slab_ number_of_points, thickness,
Chapter 11: The 3D Model - Prisms, Slabs and Extrusions 153 x1, y1, z1, status_code _1, x2, y2, z2, status_code _2, x3, y3, z3, status_code _3, ... xn, yn, zn, status_code _n
The thickness is measured vertically, not normal to the slab base. To make it easier to calculate the z values, I would suggest that you make it a rule to use the x-axis as the slab pivot line. Then the definition will become: slab_ number_of_points, perpendicular_thickness/cos(pitch_angle), x1, y1, y1*tan(pitch_angle), status_code _1, x2, y2, y2*tan(pitch_angle), status_code _2, x3, y3, y3*tan(pitch_angle), status_code _3, ... xn, yn, yn*tan(pitch_angle), status_code _n
where pitch_angle is the angle of the slab.
11.5.8
Cslab_
The cslab_ element is identical to the slab_ element, but applies a different material to top, side and base surfaces. cslab_ top_material, base_material, side_material, number_of_points, thickness, x1, y1, z1, status_code _1, x2, y2, z2, status_code _2, x3, y3, z3, status_code _3, ... xn, yn, zn, status_code _n
11.5.9
Extrude
I use the prism_ element a lot, but the extrude command is just as good. In some situations it is actually a lot better, since it does not automatically merge with neighbouring extrusions. extrude number_of_points, dx, dy, dz, mask, x1, y1, status_code_1, x2, y2, status_code _2, x3, y3, status_code _3,
154 Chapter 11: The 3D Model - Revolved Forms ... xn, yn, status_code _n
The mask controls the visibility of the top and bottom polygons and edges. mask
meaning
1
Base surface is present.
2
Top surface is present.
4
Final surface is visible if the polygon is not closed with a status -1.
16
Base edges are visible.
32
Top edges are visible.
The visibility status points are simply 1 for a curved or hidden vertical edge, 0 for a visible vertical edge, or -1 for a polygon end point. The side surface is always present, as are the top and bottom edges. The shape status values can be added to this code. When using the extrude command, be aware that any edge for which the visibility status is set to 1 will appear rounded when rendered in OpenGL.
11.6 Revolved Forms The revolve element creates a volume of revolution. It takes a poly-line, and spins it around Figure 126 – The x-axis is an axis to form a solid. It’s a lot like a virtual wood-turning lathe or potter’s wheel. Perhaps the axis of revolution for a it’s more like a lathe, as the profile is revolved about the local x-axis (figure 126). If the poly- revolved form. line is closed, the revolved solid will have a hole in the center, forming a donut-like form. If the poly-line is open, the open ends can form the top and bottom of a cylinder-like form.
11.6.1
Define a Revolved Form
The revolve definition takes a poly-line defined in a 2-dimensional r-x axis system, and revolves it around the x-axis. The solid of revolution can sweep around any angle between 0 and 360º. !Syntax to model a Revolved Form revolve number_of_points_in_the_polyline, sweep_angle, mask, x1, y1, status_1, x2, y2, status_2, ... xn, yn, status_n
Figure 127 – Use a sweep angle of 360° for a full revolved surface.
Chapter 11: The 3D Model - Revolved Forms 155 The sweep_angle controls the angle through which the poly-line sweeps. For a full ‘donut’ or ‘cylinder’, use a sweep angle of 360º. For example, create a new object with a material-type parameter donutMat. Copy the following code to the 3D Script editing window, and click on the 3D View button to create a whole donut (figure 127). !Create a fresh whole donut material donutMat revolve 2, 360, 3, 0, 0.050, 901, 0.020, 360, 4001
If you are on a diet, you might want only one-third of a donut, so use a sweep angle of 120º (figure 128).
Figure 128 – Use a sweep angle less than 360° to create a partial revolved surface.
!Create a partly eaten donut material donutMat revolve 2, 120, 3, 0, 0.050, 900, 0.020, 360, 4001
This example also illustrates how the revolve element measures its sweep angle. Look down the x-axis towards the origin, so that the arrow of the x-axis is poking into your eye. The sweep angle is measured from the y-axis in a counter-clockwise direction (figure Figure 129 – If the x-axis is 129). poking you in the eye, the The mask value controls the visibility of faces and edges on the revolved solid. If your form direction of revolution is does not sweep through the full circle, you can choose to show or hide the polygons that fill counter-clockwise. the start and finish cuts. Add mask bits together to get the desired combination of effects. mask bit
effect
0
Only the curved surface is drawn.
1
The base surface is included.
illustration
156 Chapter 11: The 3D Model - Revolved Forms 2
The top surface is included.
4
The side surface is included at the initial angle.
8
The side surface is included at the final angle.
16
Edges are visible at the initial angle.
32
Edges are visible at the final angle.
64
All edges are visible so the surface is no longer smooth.
Some examples of the effects of adding the bits are given below. For a solid slice use 1 + 2 + 4 + 8 + 16 + 32 = 63
For a complete solid use 1 + 2 + 16 + 32 = 51
Chapter 11: The 3D Model - Tubes 157 For a donut use 16 + 32 = 48.
Each edge of the poly-line is defined by x-y co-ordinates and a status value. For smooth edges, use the status value 1. For sharp edges, use the status value 2. Using the status value 0 works fine in the ArchiCAD internal rendering engine, but if you use OpenGL the edges will look strangely smoothed.
11.6.2
Negative Y Values
One common error when defining a revolved form is to get the poly-line x- and y- axes mixed up. Because the poly-line is revolved around the x-axis, negative values of y are not permitted. However, you can use negative x-values. If your poly-line crosses the x-axis, an error message will appear (figure 130), and the GDL script will fail to generate a 3D model.
Figure 130 – Negative y-values for
It is not always easy to find which edge of the poly-line crosses the x-axis. Be aware that a revolved poly-line are not tolerated by the GDL interpreter. even a zero value will result in failure. When using arc-type edges in the poly-line, it is not always easy to know whether or not these will cross the x-axis. If they do, you will still get the error message, but its more difficult to find the offending edge. For example, the following script looks OK but results in the error message: !Create a fresh donut revolve 2, 180, 3, 0, 0.050, 900, 0.051, 360, 4001
In this case, there’s only one edge, so its relatively easy to see that the problem is the arc which crosses the x-axis. When using the revolve element, it’s a good idea to include error handling to ensure that this type of error does not happen. In some cases it’s difficult to ensure that errors can never happen, but it is often possible to provide at least partial error handling for specific cases that are known to cause problems.
11.7 Tubes GDL provides two elements that can be used to extrude a profile along a path. The tube element can be used to create a tube of constant cross-section. It is great for running extrusions around paths to create picture frames, handrails, and the
158 Chapter 11: The 3D Model - Tubes like. The sweep element is similar but allows the cross-section dimensions to change along the path. In this section we will discuss only the tube element. If you want to try using the sweep element, you will find a description in the GDL Reference Manual – personally, I never use this element.
11.7.1
Using the Tube Element
To use the tube element, you must define a cross-section anda set of nodes that define the path. The cross-section is extruded along the path to form the tube. Use the following GDL syntax to model a tube. !Syntax to model a Tube tube number_of_profile_points, number_of_path_nodes, mask, profile_point_x1, profile_point_y1, profile_point_status_1, profile_point_x2, profile_point_y2, profile_point_status_2, ... path_node_x1, path_node_y1, path_node_z1, path_node_angle1, path_node_x2, path_node_y2, path_node_z2, path_node_angle2,
Figure 131 – A tube can become distorted if its path turns out of Masking values control the presence of surfaces and edges at the start and end of the tube. the original plane at an oblique You can also choose to give the tube a segmented rather than a smooth surface. Use a direction. ...
mask value of 3 for a solid tube, plus 16 for visible edges on the start surface, and an extra 32 for visible edges on the end surface. The profile or cross-section is a poly-line similar to that used in by the prism_ element. It is defined by a list of co-ordinate pairs and status values. The path is defined by a list of (x, y, z) co-ordinates. Each co-ordinate has an associated angle, which can be used to deliberately twist the tube. This is somewhat ironic as generally the biggest challenge of the tube command is to avoid distortion.
11.7.2
Distortion
The tube is quite an impressive tool. It’s robust, almost free-form, and can be used for a Figure 132 – If the tube turns out wide range of modeling tasks. But it’s not magic. For example, it’s physically impossible to of its original plane at 90°, no create a tube of constant cross-section along a path that suddenly turns out of the plane at distortion occurs. an oblique angle. In this case, the tube becomes distorted, which is understandable.
Chapter 11: The 3D Model - Tubes 159 To define a tube, you must specify a profile (the cross-section of the tube), and a path along which the profile will sweep. At each path node, the tube cross-section forms a miter with the path legs that enter and exit the node. The path therefore requires a leadin leg, and an exit leg. The first and last path legs do not contribute to the body of the tube, but define the cut-off angle at the tube end. If every path node is confined to a plane, the profile is always perpendicular to the path so no twisting will occur. As soon as a point on the tube is placed outside of the original plane, the tube may have to twist (figure 131). It will not twist, however, if it moves out of the plane in a direction normal to the plane (figure 132).
11.7.3
Smooth Curves
You can create paths that include smooth curves (figure 133). Tubes are automatically smoothed provided the angle between two adjacent sections is shallow. It’s difficult to say what the cut-off angle between smooth and sharp is, but it seems that 10° is considered shallow. I find it best to use the stack when defining curved paths. See the chain link example below for a smooth, continuous curve.
11.7.4
Figure 133 – These monkey bars include smooth radiused bends.
Continuous Loops
To define a continuous loop, ensure that the first and last visible points coincide, that the lead-in section aligns with the last visible section, and that the lead-out section should align with the first visible section. If the ends are tangential, use the mask value to remove the visible line around the end surfaces. It’s best if the end surfaces meet midway along a straight section of the path, or at a sharp corner. If they meet on a curve it’s impossible to control the edge visibility. For example, a chain link can be modeled using a tube as in the sub-routine below (figure 134). "01 Chain Link": resol 8 R01 = linkThick/2 W01 = (linkWidth - R01)/2 L01 = max(linkLength - R01, 2*W01 + R01) !Define a path !Start point and lead-in put 0, -W01, 0, 0, L01/2, -W01, 0, 0
Figure 134 – Each link of this chain is formed using a tube with a closed path.
160 Chapter 11: The 3D Model - Tubes !Far end curve for q01 = -90 to 90 step 15 put L01 - W01 + W01*cos(q01), W01*sin(q01), 0, 0 next q01 !Near end curve for q01 = 90 to 270 step 15 put W01 + W01*cos(q01), W01*sin(q01), 0, 0 next q01 !End point and tail-out put L01/2, -W01, 0, 0,
L01, -W01, 0, 0
!Run a round section around the path tube 2, nsp/4, 3,
Figure 135 – Textures do not map well to tubes.
0, 0, 901, R01, 360, 4001, get(nsp) return
11.7.5
A Note of Caution
Take care when defining curved paths, especially when the tube profile is also curved. This can result in a very large number of surfaces. Our single chain link has two end surfaces. Each section of the path has 16 triangular surfaces (since the resolution has been set to 8). There are two curved ends each with 12 sections, and one of the two straight sections is broken into 2 to provide a smooth join. This gives a total of 2 + 16*(2*12 + 1) = 402 triangular polygons that define the surface of the chain link. By default the resolution applied to arcs is 36. Had we not explicitly set the resolution, the number of polygons would have been 2 + 72*(2*12 + 1) = 1802 polygons! If we had used more sections in the path, the number of polygons would have been even higher. Of course, a complete chain needs many links. For a chain including 100 links, our link of 402 polygons would use 40,200 polygons while the 1802 polygon link would result in a massive 180,200 polygons! All this for a mere chain, which may be a small design feature or possibly part of an element for visualization. You can see that it’s easy to create objects that use massive numbers of polygons. We’ll look at ways to avoid this later.
11.7.6
Texture Mapping
Later we’ll look at mapping textures to 3D elements. Most GDL elements (cylinders, cones, prisms and the like) automatically map textures to their surfaces in a sensible way. Suffice it to say here that texture mapping does not work well for tubes (figure 135).
12 Groups and Solid Element Operations This chapter covers two related topics – group operations and solid element operations. Both of these use one set of 3D elements (the operator set) to modify another (the target set). When designing a GDL object, you should take into consideration whether it is likely to be used as an operator in a solid element operation, and if so, how it should behave. For example, a kitchen sink object should be designed to be used as an operator that will cut a hole in the bench into which it is placed. The shape of the cut hole should not necessarily match the shape of the sink. The manufacturer may specify a tolerance. Also, the hole will likely be cut using a straight bladed saw, so is unlikely to fit to the exact contours of the sink. Similarly, an object may include detail that surrounding structures should not allow for. You may want to cut a hole in a wall to allow a rectangular hollow section steel beam to penetrate. If the 3D beam was used as a solid element operator to cut the hole, the solid steel would remove part of the wall, leaving a plug of wall filling the hollow part of the steel section. In this case we really wanted the whole volume removed. Other objects include a main body with added accessories. When we use such objects as solid element operators, we may want only the main body to be included in the operation. In the 3D Script, you can perform group operations between sets or groups of GDL elements. Group operations are similar in many ways to solid element operations. You must define a group of target elements, a group of operator elements, and an operation to perform. The resulting group can be placed to form part of the 3D model. In this chapter, we will see how to create a cutting form for use in solid element operations that is different from the visible 3D model. We will also look at how to define groups of elements that can be added, subtracted and intersected within the GDL script.
12.1 Solid Element Operations When you design an object, you should bear in mind that the object may at some stage be used as the operator of a solid element operation. Of course, if you are designing a clock, or a musical instrument, it is highly unlikely that anyone will subtract it from a roof, wall or slab. In such cases it is fairly safe to ignore the possibility. On the other hand, if you are designing a kitchen sink, it is much more likely that someone will want to use the object as the operator in a solid element operation. In such cases you will want to modify (usually simplify) the shape of the object when it is used to create a cut. It turns out that you can use the global parameter glob_context to test whether the object is being used as a cutting body. This allows you to model the object using one 3D form, and use a different 3D form (perhaps a simplified version) to use as a cutting body in solid element operations.
162 Chapter 12: Groups and Solid Element Operations - Solid Element Operations The value of glob_context in cutting mode is between 40 and 49. Try it Yourself: A Cylinder that cuts a Square Hole To see how this works, create a new object, and copy the following script into its 3D Script editing window. !A Round Peg in a Square Hole if glob_context > 40 and glob_context < 50 then !Use the body to cut away a square hole !when the object is used !in a Solid Element Operation add –a/2, -a/2, 0 block a, a, zzyzx del 1 else !Round Peg for the Regular 3D Model cylind zzyzx, a/2
Figure 136 – A round peg and a rectangular slab. No Solid Element Operation has been applied at this stage.
endif
Copy the following script into the 2D Script editing window. !A Round Peg in a Square Hole rect2 –a/2, –a/2, a/2, a/2 circle2 0, 0, a/2
Set the value of A to 0.100m (4”) and the value of ZZYZX to 1m (3’). Finally, save the object into your practice library. In the project plan view window, place a slab then place an instance of the new object onto the slab. Adjust the slab so that the object is fully contained within its area. Generate a 3D view, and if necessary drag the object up or down so that it penetrates the slab. You will see that the object is simply a round peg (figure 136). Use the Solid Element Subtract tool, with the object as operator and the slab as target. Figure 137 – When a Subtract Click the Execute button to cut a hole in the slab. You will see that the cut hole is square operation is applied, the round peg cuts a square hole in the slab. (figure 137). This approach allows us to create intelligent objects that cut away the necessary material around them to leave room for installation.
Chapter 12: Groups and Solid Element Operations - Defining a Group of Elements 163
12.2 Defining a Group of Elements Within the GDL script you can define groups of elements that can be used as targets and operators to perform solid element type operations. To define a group of elements within a 3D Script, use the following syntax. !Syntax to define a Group of Elements group “group_name” first_GDL_element second_GDL_element ... last_GDL_element endgroup
12.2.1
Group Name
The group name must be unique. There can be no other group in your script with the same name. If there is another group with the same name, ArchiCAD will pop up an error message when you try to save the object.The group name should be meaningful, so that when you read through a script, you can understand what the group is intended to model. If it is modeling the interior surface of a hand basin, by all means use the name “Basin – Interior” for the group.In some cases, you will want to define a set of groups within a loop. To do so, you could force each group to have a unique name by adding a counter to its name as “group_name_” + str(counter, 1, 0).
12.2.2
Elements and Transformations
All the elements of the group are contained between the group and endgroup statements. Within the group definition, you can include any GDL element you like (prisms, spheres, tubes, etc.). You can even use previously defined groups within a group. You can also include transformations within a group definition. These transformations are scoped to that group and are implicitly cancelled by the endgroup statement. However, I would recommend that you use the del command to explicitly cancel the transformations anyway – it makes for a tidier, more readable script. For instance, the following group definitions are OK. group “Cube” brick 1, 1, 1 endgroup group “Sphere” add 0.5, 0.5, 0.5 sphere 0.6 del 1 endgroup
164 Chapter 12: Groups and Solid Element Operations - Placing a Group
12.3 Placing a Group Create a new object and copy the example GDL script from the previous section into the 3D Script window. When you click on the 3D View button to run the script a warning message will pop up (figure 138). We defined two groups, but until we place them, they won’t contribute to the 3D model. To place a group, use the command: placegroup group_name
The group will be placed at the local origin. You can place the same group in multiple locations, and at different sizes and orientations. Just perform a transformation to adjust the local axis system, then use placegroup to place the group.
Figure 138 – When you define a group of 3D elements, it is not automatically placed in the model. You must use the placegroup command to place that group of elements.
12.4 Nested Groups You cannot nest group definitions. In other words, you cannot put one group definition inside another. The following GDL script will not work. If you try to run this script, a warning message will pop up (figure 139). group “Cube” brick 1, 1, 1 group “Sphere” add 0.5, 0.5, 0.5 sphere 0.6 del 1 endgroup endgroup
You can, however, call a macro from within a group definition. The macro itself can contain group definitions. You can also include one group as an element of another group, provided it has been previously defined. We’ll see how this works in the next section.
12.5 Group Operations So far, we have defined groups and placed them. Really we have achieved
Figure 139 – You cannot include one group definition within another.
Chapter 12: Groups and Solid Element Operations - Group Operations 165 nothing that could not have been done without using groups, and by simply calling a sub-routine. In this section we will start to use the real power of the groups, and use one group to modify another. To add two groups together, use the command: result_group_name = addgroup(target_group_name, operator_group_name)
To create the intersection of two groups, use the command: result_group_name = isectgroup(target_group_name, operator_group_name)
To subtract one group from another, use the command: result_group_name = subgroup(target_group_name, operator_group_name)
The resulting group is stored in the variable result_group_name. Try it Yourself: Use Group Operations to Create a Wedge of Cheese As an example, create a new object with a material type parameters cheeseMat and rindMat. Copy the following GDL script into the 3D Script editing window. tol = .0001 toler .001 !A cylinder to model the complete round of cheese material rindMat group "Round" cylind zzyzx, a endgroup !A wedge-shaped prism material cheeseMat group "Wedge" prism 4, zzyzx, 0, 0, a, 0, 2*a*cos(30/2), 2*a*sin(30/2), a*cos(30), a*sin(30) endgroup !Spherical holes placed randomly in the cheese group "Holes" dhi = .2 for hi = 0 to zzyzx step dhi h = hi + dhi*(.5 - rnd(1))
166 Chapter 12: Groups and Solid Element Operations - Group Operations dri = dhi for ri = 0 to a step dri r = ri + dri*(.5 - rnd(1)) if abs(r) > tol then dqi = (dhi/r)*(180/pi) for qi = 0 to 30 step dqi h = h + dhi*(.5 - rnd(1)) r = r + dri*(.5 - rnd(1)) q = qi + dqi*(.5 - rnd(1)) holeRadius = .02 + rnd(.11) if h - holeRadius > 0 and h + holeRadius < zzyzx and r + holeRadius < a then add r*cos(q), r*sin(q), h sphere holeRadius del 1 endif next qi endif next ri next hi endgroup !Group Operations !Intersect the Round with the Wedge cheese = isectgroup("Round", "Wedge") !Subtract the Holes cheese = subgroup(cheese, "Holes") !Place the resulting wedge of holey cheese placegroup cheese
Click the 3D View button to run the script. The cylinder of the group named “Round” has been trimmed by the group named “Wedge” to form a wedge shaped block of cheese. Randomly located spherical holes have been punched in the cheese using the group named “Holes”. The resulting model looks nearly good enough to eat. Figure 140 – A wedge of cheese
You will notice that the model takes a while to generate. The reason for this is that it created by subtracting a group of has to calculate the intersections of numerous curved surfaces (the spherical holes). spherical holes from a group that This translates to thousands of operations required to calculate the 3D form of the contains the wedge. cheese. When using group operations it is easy to create a high processing demand.
13 Text Text frequently forms a part of an object’s 2D symbol. This is clearly true in the case of objects such as labels, door & window markers, and title blocks that are specifically designed to display text. Many other objects also display text as part of their 2D symbol. Identifiers or codes can help specify the exact type of plumbing fixture to be installed, indicate the number of switches on an electrical plate, or display a room name on a zone. In a schedule, or a key noting system, lists of formatted blocks of text can be displayed in a table made up of rows and columns. In the 3D model you might include signage or billboards. The text in these objects may be solid, 3D blocks or planar surfaces in the shape of characters. In this chapter, I will show you how to define a GDL text style. We’ll look at how to allow the end user to select a font. We’ll then move on to use the text2 command to place a single line of text, place rich formatted paragraphs of text in blocks. Finally we’ll look at how to use text as part of the 3D model.
13.1 Selecting a Font ArchiCAD uses real text with regular fonts such as Arial, Times and Helvetica. This looks very crisp compared to the generic fonts used by some other CAD systems. If you want to include text in your object, you must provide a user interface for font selection. To present a list of available fonts to the end user, you should use the request FontNames_List. Try it Yourself: List Available Fonts To see how this works, create new object and add a string parameter with the variable fontName (figure 141). Copy the following GDL code into the Parameter Script. !List all available font names dim fontNames[] rrr = request (“FontNames_List”, “”, fontNames) values “fontName” fontNames
The script declares a 1-dimensional array variable (i.e. a vector) Figure 141 – Create an object and add a string type fontNames[]. It then uses the FontNames_List request to parameter with variable fontName.
168 Chapter 13: Text - Define a Text Style populate the array with available font names. Finally the script uses the values command to apply the array elements to the values list for the fontName parameter. Now look at the parameter list. You’ll see that the fontName parameter now has a list of fonts attached to it (figure 142). The “FontNames_List” request returns a complete list of every available font. On a Windows platform, this includes all substituted font types (these are prefixed with the ‘@’ character). It also includes all combinations of font and script types, such as Arial Western, Arial Cyrillic, Arial Baltic and the like. The Figure 142 – The Parameters script finds and lists all available fonts. list thus tends to be rather long and unwieldy. 15
13.2 Define a Text Style To define a text style you must specify a font, the height of the text, an anchor point (alignment), and effects such as bold, underlined and italics.
Figure 143 – Text style includes font, size, orientation and effects such as bold, italic and underlined text.
15
Its easy enough to populate a values list with the complete list of available fonts, but this approach tends to be clumsy for the end user. The first edition of the GDL Handbook outlined a method that used a macro to return either a list of the different font types, or (for a given font type such as Arial) all available script types (Western, Cyrillic, etc.). The macro also filtered out substituted fonts beginning with the ‘@’ character. I’ve removed this from the second edition, as it depends on the file input/output commands. These commands have become problematic since the advent of Windows Vista. Cadimage now uses an add-on based approach to provide a short list of fonts and script types.
Chapter 13: Text - Define a Text Style 169 Use the command define style to define a text style. This command takes a number of arguments. define style style_name font, text_size, anchor point, effects
The style_name value can be any text string you choose, as long as it is unique. I would recommend that you choose a style name that describes how it is to be used (e.g. “Title”), or that is descriptive of the style (e.g. “Bold”, “Underlined” or “Plain”). That way, you will know which one to use. When you come to a situation that you need bold text, you’ll use “Bold”. If you want a title, you’ll use “Title”, and so on. It also makes it much easier to read the script later on – you will understand what is going on at a glance. The font should be one of the available system fonts. It is usually a parameter variable, so that the user can choose her favorite font. A list of the fonts can be obtained as described in the previous section on Selecting a Font. Text_size is defined independent of the drawing scale, since text must stay the same size (so as to be readable), whether the drawing is a 1:5 detail or a 1:1000 site plan 16. The unit used by the GDL interpreter for text size, is millimeters. Typically, a real number parameter with no pre-defined unit is used to set the text size value.
The anchor_point is a number between 1 and 9 that determines how the text is anchored to the placement point (figure 144). For example, if you want the text box to center itself on the placement point, use the anchor point index 5. The effects value is an integer obtained by summing the following numbers: •
Add 1 if the text is to be bold.
•
Add 2 if the text is to be italicized.
•
Add 4 if the text is to be underlined.
Figure 144 – Anchor points for the text style.
For example, for text that is bold and italics use the effects value 1 + 2 = 3. For plain text, use the effects value 0. Try it Yourself: Define Text Styles To illustrate how all this works, we’ll define two text styles. One will be a 6pt, bold Arial font, anchored at the center. The other will be a 3mm, italicized and underlined Times font, anchored at the bottom left. Type the following script into the 2D Script editing window of a new object. 16
While it is true that you can use a scaling text size in a text block, I would not recommend it unless you have a particular reason for using it. Generally, you should choose a real-life text size. That way, you can be sure it will be readable when it is printed out.
170 Chapter 13: Text - Text Size !Define a 10pt, bold Arial font, anchored at the center define style “Arial 10pt Bold Centered” Arial, 10*0.3528, 5, 1 style “Arial 10pt Bold Centered” hotspot2 0, 0 text2 0, 0, “Arial 6pt Bold Centered” !Define a 3mm, italicized, underlined Times font, !anchored at the bottom left define style “Times 3mm Italics ULined Bottom Left” "Times New Roman", 3, 7, 2 + 4 style “Times 3mm Italics ULined Bottom Left” hotspot2 0, -0.500 text2 0, -0.500, “Times 3mm Italics ULined Bottom Left”
Click on the 2D Full View button to view the result (figure 145).
Figure 145 – Text drawn using different styles.
13.3 Text Size Traditionally, two units are used for text size, namely millimeters and points. Millimeters is the natural choice for people who use the metric system. For people who use feet and inches, though, points is the expected unit for text size. It’s a good policy to allow the user to input the data as measured in either format, and then (if required) automatically convert the number into millimeters for use by the GDL script. We can use the request Linear_Dimension to check which system is being used on a particular project. rrr = request(“Linear_Dimension”, “”, linear_dimension_format)
The returned value of the variable linear_dimension_format is a text string. Possible values that may be returned include: •
%m – meters
•
%cm – centimeters
•
%mm - millimeters
•
%ffi – feet and fractional inches
•
%fdi – feet and decimal inches
•
%df – decimal feet
On examination of the returned value, we can see that all the metric units have the character ‘m’ somewhere in the returned string. We can use this as a flag to tell us whether to display points or millimeters in our user interface.
Chapter 13: Text - Place a Single Line of Text 171 If the project settings are for the metric system (the returned value contains the character m), we display the textSizeMetric parameter. If the project settings are for feet & inches (the returned value does not contain the character m), we display the textSizeImperial parameter. Whenever a value is entered into the textSizeImperial parameter, the corresponding value of textSizeMetric is calculated and stored. Similarly, if the user changes the value of textSizeMetric, the corresponding value is calculated for textSizeImperial. This allows the user to use the units with which she is familiar. To convert points to mm, use the factor 0.3528. For example, 10 points would convert to 3.528mm. In the GDL script, the size is always assumed to be mm, so use the textSizeMetric value when defining a text style.
13.4 Place a Single Line of Text Some objects use predictably short blocks of text, such as IDs or numbers, where no text wrapping is required. In cases like this, you can use the text2 command. This command simply places a single line of text at a given (x, y) anchor point co-ordinate. For example, having previously defined a text style, you might add the following line of GDL script to the 2D Script of an object: !Place the object’s ID text2 0, 0, glob_user_ID
This uses the text2 command to place the object’s user ID at its origin. Example Try it for yourself. Create a new object named Hello World, and add a text-type parameter textToDisplay to its Parameters list. Give it the default value “Hello World”. Type the following GDL code into the 2D Script window. !Define a text style define style "Normal" Arial, 2, 1, 0 style "Normal" !Display the text on a single line text2 0, 0, textToDisplay
Now click on the 2D Full View button to see the result (figure 146).
13.4.1
Width of the Text Line
Figure 146 – Hello World!
In some cases it can be useful to know the dimensions of the line of text you have placed. You might want to draw a border around the text, or expand the column of a table to fit the text. To find the width of the line, use the stw() function. This function returns the width of the text expression of its argument. The width depends on the style you
172 Chapter 13: Text - Define a Paragraph have set. Add the following lines of code at the end of the 2D Script of your Hello World object. !Find the width of the displayed text textWidth = stw(textToDisplay)*glob_scale/1000 !Draw a border around the text rect2 0, 0, textWidth, -3*glob_scale/1000
Figure 147 – A rectangular
Now click on the 2D Full View button again to see how the border fits to the text (figure border that fits tightly around the text. 147). There are some points here that deserve explanation: •
The text width is returned in millimeters. We used the conversion factor glob_scale/1000 to convert from millimeters to meters, then to represent that measurement at life size on a scaled drawing.
•
Figure 148 – The loop of the ‘g’
There is no way to find the exact height of the characters when placing a single line of is closer to the margin than the text. Even though we set our style up with a text height of 2mm, we had to place a other characters. 3mm border around it. A rule of thumb might be to use 1.5 times the stated text height as we did here. But it’s not perfect. See what happens if you change the value of textToDisplay to read ‘Greetings Humans!’ The loop of the lower case ‘g’ sits on top of the border, with no margin, while a slim margin is visible around the other characters (figure 148). If you change to a different font, things can get quite messy. Here I changed to a font Figure 149 – Odd fonts such as named Giddyup Std, which has its own quirks (figure 149). Giddyup Std include characters To avoid the issue, you could provide parameters for margins around the text.
that extend beyond the reported text height.
13.5 Define a Paragraph For anything that requires word wrapping, tabulation, or use of different fonts within the text block, you should use the richtext2 command. In order to use richtext2, you first need to create a set of one or more paragraphs. Having created the paragraphs, you can include them in a text block. Finally, you can place the text block using the richtext2 command. A paragraph is defined using several lines of code. In summary: •
The first line defines the start of the paragraph, and some global settings that will be applied to all the text contained
Chapter 13: Text - Define a Paragraph 173 in the paragraph. •
All the text of the paragraph follows, including commands to set pen colors and text styles.
•
The paragraph is terminated using the endParagraph command.
Initializing a Paragraph The paragraph is initialized using the paragraph command. paragraph paragraph_name paragraph_alignment, first_line_indent, left_indent, right_indent, line_spacing_factor, optional_tab position_1, optional_tab_position_2, …
The value paragraph_name is a string that provides a unique identifier for the paragraph. It can be either a variable value or a constant enclosed in quotation marks. The paragraph_alignment value gives control over the alignment of the paragraph text. Set the value to 1 if the paragraph is to be left aligned, 2 to center the text, 3 to align the text right, and 4 for fully justified text. The first_line_indent, left_indent and right_indent values control the indents to apply to the first line, and left and right of the paragraph. These indents are measured from the edge of the text block in which the paragraph will be placed. Note that the first line indent is not measured from the left indent, but from the text block. Also, note that the first line indent really only applies to left-aligned or justified text. The line_spacing_factor value controls the line spacing relative to the font size in use. For example, use a value of 2 for double spacing, or 1 for single spacing. You can use different font sizes within a paragraph, and the line spacing will adjust accordingly. The final values are optional tab_positions. If you use the \t character within the paragraph text, the following text will snap to the next tab space. The tab positions are measured from the edge of the text block. By default, tab spaces are set at every 12.7mm (half inch). Terminating the Paragraph When you have added all the text to be included in the paragraph, terminate the paragraph using the GDL command endParagraph. Paragraph Text The body of the paragraph comes betwen the paragraph initialization and the endParagraph command. The body text is made up of any number of phrases, text strings which are later stitched together to form a complete paragraph. Each phrase must contain fewer than 256 characters (this is the limit for any GDL text string). This limit is sufficient for most sentences, but very long sentences must be broken into fragments.
174 Chapter 13: Text - Define a Paragraph A pre-defined text style and a pen color can be assigned to each phrase. To assign a previously defined style to a set of phrases, use the command set style. (Using the command style is not sufficient – you must include both words). All phrases following this command will take the specified text style, until another style is specified. To assign a pen color to a set of phrases, use the command pen. All phrases following this command will take the specified pen color, until another pen is specified. Create a new object, and type the following GDL code into the 2D Script window. !Define text styles to use in the paragraph define style “Header 1” Arial, 4, 1, 0 define style “Body Text” Garamond, 2, 1, 0 define style “Bold” Garamond, 2, 1, 1 !Define a paragraph paragraph “Paragraph 1” 4, 0, 0, 0, 2 !Header !Set the text style to ‘Header 1’ set style “Header 1” !Set the pen to 5 pen 5 !Place the header text “First we place the header\n” !Body text !Set the style to ‘Body Text’ set style “Body Text” !Set the pen to 1 pen 1 !Place the body text as phrases “The body text will all be written in ‘Body Text’ style, using the pen 1 color. “ “If you want to change the pen color, that’s easy. “ ”Simply add the GDL command ‘pen’ followed by the pen index.” !Set the pen to 2 pen 2 !Continue placing phrases using the new pen colour “In this case, we changed to pen 2. “
Chapter 13: Text - Define a Paragraph 175 ”Now all the text is in a different color. “ “Now it’s time to change back to pen 1, and maybe we should use a bold font.” !Set the text style to ‘Bold’ set style “Bold” !Set the pen to 1 pen 1 “Now all the text is bold. ” “You can add line breaks by using the backslash – n characters like this.\n\n\n” “Or snap to the next tab position using the backslash – t character like the following tabulated values.” “1\t2\t3\t4\t5\t6” endParagraph
As you can see from the example, the character combination \t can be used to specify a tab space, while \n defines a line return. For the moment, we can’t actually view the paragraph we just made. It has been defined, and is available for use within a text block which can be placed using the richtext2 command as we will see in the following sections. To see how our paragraph will look, and without any further explanation at this point, type the following script after the paragraph definition. !Include the paragraph in a text block textBlock "Block 1" 100, 1, 0, 1, 1, 0, "Paragraph 1" !Place the block using the richtext2 command richText2 0, 0, "Block 1"
Click on the 2D Full View button to preview the paragraph we just defined (figure 150). You can see how much more fun it is to use paragraph formatting rather Figure 150 – A paragraph can include text of than single lines of text. A very important note …
different styles and colours.
A bizarre quirk of the paragraph structure is that you cannot directly use a variable in place of a phrase. To use a variable value within the paragraph definition, you must add the empty string before it. For example, the following paragraph definition will not work.
176 Chapter 13: Text - Using Paragraphs define style “Bold” Arial, 2, 1, 1 txt = “The quick brown fox jumps over the lazy dog.” paragraph “Quick Brown Fox” 4, 0, 0, 0, 2 set style “Bold” txt endParagraph
To force the script to work, make the following change. define style “Bold” Arial, 2, 1, 1 txt = “The quick brown fox jumps over the lazy dog.” paragraph “Quick Brown Fox” 4, 0, 0, 0, 2 set style “Bold” “ “ + txt endParagraph
All we did was to add an empty string in front of the txt variable. But that’s all it takes. Without that crucial empty string, failure is guaranteed.
13.6 Using Paragraphs In the previous section we learned how to define a paragraph. In this section I’ll show you how to include a paragraph as part of a text block, then use the richtext2 command to place the text block into the 2D symbol. Create a Text Block Use the command textBlock to create a block of text made up of one or more paragraphs. textBlock text_block_name text_block_width, text_block_anchor, text_block_angle, character_width_factor, character_spacing_factor, scale_independent, string_expression_1, string_expression_2, string_expression_3, …
The text_block_name is a unique string that identifies the text block. You can choose any string you like, so long as it is unique. The string may be a variable. The value you give to text_block_width sets the width of the text block in millimeters.
Chapter 13: Text - Using Paragraphs 177 Use the text_block_anchor value to set the anchor point of the text block. It works in the same way as a regular single-line text anchor (figure 151). For example, using anchor value 1, the text block will be positioned so that its top, left corner is at the placement point. Set the text_block_angle value to place the text block at an angle to the horizontal. A value of zero will place a horizontal text block. A value of 90 will place a text block rotated so the text runs up the page. To change the character width and spacing, set the character_width_factor and Figure 151 – Anchor point values for a character_spacing_factor values. These values are set relative to the regular text block. character width and spacing. Set the values to 1 for regular text. Set the character_spacing_factor to 2 if you want characters that are twice the regular width. I would recommend that you set the scale_independent value to 1. This value is a flag that tells the text block to use scale-independent dimensions. This means that your text will be readable whatever the drawing scale. The final items in the text block definition are the various string_expressions_ that are to be included in the block. Here you should list the names of any Figure 152 – If you include a comma paragraphs you want to include. between the textblock name and width Be especially careful not to include a comma between the text_block_name and values, an odd warning message will come up. text_block_width values. If you do add a comma here, an odd alert message will pop up when you run the Check Script test, and the text block will not be created correctly (figure 152). Placing the Text Block To place the text block, use the richText2 command. richText2 x_co-ordinate, y_co-ordinate, text_block_name
The text_block_name value should be the name of a previously defined text block. Text Block Size If you are placing several text blocks together, you will need to know the size of each text block. You can request the size of a text block using the Textblock_Info request.
178 Chapter 13: Text - Text Orientation !Request the size of a text block rrr = request(“Textblock_Info”, text_block_name, textBlockWidth, textBlockHeight)
In this request, you pass in the name of the text block as the text_block_name value. The request sets the values of the return variables textBlockWidth and textBlockHeight to be the width and height of the text block. The returned values will be in millimeters if you defined the text block as scale_independent.
13.7 Text Orientation Nobody likes having to stand on their head in order to read text on a drawing. It’s just not comfortable. As far as possible, then, text should be aligned horizontally, reading from left to right. In some cases (for example when Figure 153 – Text orientation. dimensioning plans) it’s necessary to rotate text to align with other drawing elements. In such situations, you should take care that the text remains readable, sitting above the base line. Where text is vertical, it should run up the page. Depending on the requirements of the object you are scripting, you should either force the text to remain horizontal, or at least ensure that it always remains readable. If you simply place a text in the 2D Script without taking the object’s orientation into account, then if you rotate the object, the text orientation will become unreadable (figure 154). Horizontal Text When an object or label is placed in an ArchiCAD drawing window, it is given an orientation and mirror status. In other words, the object may be rotated through an angle from the horizontal, and possibly flipped. Since ArchiCAD 13, it has also been possible to rotate any view. This allows the architect to work with and eventually print that view at a different angle. We should also take this into account so that the text treats the view orientation as Figure 154 – Dumb text orients itself in the being horizontal. In order to force the text to remain ‘horizontal’ (i.e. aligned to the view grid), we can apply transformations that cancel the object placement orientations and apply the view rotation. If the object was mirrored, we apply a mirror to flip it
direction of travel.
Chapter 13: Text - Text Orientation 179 back to its natural sense. If the object was rotated, we apply the same rotation in the opposite direction. This will leave the local axis system parallel with the global system. We then apply the view rotation, and we’re done. The following code can be used to force text to be horizontal. rrr = request(“View_Rotangle”, “”, viewRotation) mul2 1 – 2*symb_mirrored rot2 –symb_rotangle + viewRotation ... use text2 or richtext2 to place the text ... del 2
In this script fragment, global variables symb_mirrored and symb_rotangle are used to retrieve information about how the object is oriented. To see how this works, create a new object and type the following GDL script into the 2D Script editing window. !Place annoyingly horizontal text define style “Normal” Arial, 2, 5, 0 style “Normal” hotspot2 0, 0 viewRotation = 0 rrr = request(“View_Rotangle”, “”, viewRotation) mul2 1 – 2*symb_mirrored, 1 rot2 –symb_rotangle - viewRotation text2 0, 0, “Try to rotate this annoying text!” del 2
Save the object as Annoying Text. Now place an Annoying Text object into an ArchiCAD project, and try to mirror or rotate it. Try as you might, you will fail! It remains stubbornly horizontal. You could extend the Annoying Text object to allow the user to set an angle for the text, or to flick between horizontal and vertical text (figure 155). Angled Text Text is readable when placed between angles -90º and 90º. We’ll use an interval of -89º < θ < 89º. This allows for a little inaccuracy by the user in placing vertical text. If the object’s rotation angle is outside the readable range, we should add Figure 155 – This text defies all attempts 180º. at rotation. mul2 1 – 2*symb_mirrored
180 Chapter 13: Text - Text Orientation viewRotation = 0 rrr = request(“View_Rotangle”, “”, viewRotation) mul2 1 – 2*symb_mirrored, 1 if symb_rotangle > 91 and symb_rotangle < 271 then rot2 180 else rot2 0 endif ... use text2 or richtext2 to place the text ... del 3
Just as in the case of horizontal text, we first neutralize the object’s mirror status. We can then use the global variable symb_rotangle to retrieve the object’s rotation angle. Depending on its value, we can make the necessary Figure 156 – This text remains readable while allowing for rotation. adjustments. To see how this works, make a new object called Smart Text and type the following GDL script into its 2D Script editing window. !Place less annoying text define style “Normal” Arial, 2, 5, 0 style “Normal” viewRotation = 0 rrr = request(“View_Rotangle”, “”, viewRotation) mul2 1 – 2*symb_mirrored, 1 rot2 –symb_rotangle + viewRotation if symb_rotangle > 91 and symb_rotangle < 271 then rot2 180 else rot2 0 endif text2 0, 0, “Smarter text.” del 3
Place the object into an ArchiCAD project and rotate and mirror the object (figure 156). However you rotate or mirror the text, it always remains readable.
Chapter 13: Text - Text in 3D 181
13.8 Text in 3D Text is most commonly used when annotating drawings, but it can also have a place in modeling the virtual building. 3D text can be used wherever signage should appear on elevations, interior elevations, or hidden line 3D drawings. For example, it may be important to show the location of a mandatory lighted exit sign above a doorway. It would be great if you could place the sign as a 3D object into your virtual building. It would then show up whenever you created an interior elevation, a 3D flythrough, a presentation perspective, or a virtual reality rendering.
13.8.1
The text command
Figure 157 – 3D text can be used for signage.
If you want to include text elements in the 3D Script of an object, use the GDL text command. This command converts each character in the text to a 3D block, with a thickness. The text command takes 3 values. !3D Text text text_thickness, 0, text_string
The text thickness is the thickness (in meters) of the blocks used to represent the text. The zero value is not used. The third value is the text you want to display.
13.8.2
Text height
When you define a text style to use with 3D text, you should set the height using millimeters as the unit. If you want text that is 300mm high, you should set the style height to 300. The3D text element is unique in this respect. Try it Yourself: Create 3D Text To see how this works, create a new GDL object with: •
A text parameter that has variable textStr.
•
A material parameter that has variable textMat
•
A pen parameter that has variable textPen.
Type the following script into the 3D Script editing window. !Create a 3D text string
Figure 158 – By default the text can only be read from above.
182 Chapter 13: Text - Text in 3D material textMat pen textPen define style "3D" "Times New Roman", 300, 5, 0 style "3D" text 0.100, 0, textStr
Click on the 3D View button to see the result (figure 158). The resulting 3D text is 0.300m high and 0.100m thick. As you can see, the text is placed lying flat. To make it stand up, make the following Figure 159 – Text transformed changes to the 3D script. !Create a 3D text string
into the vertical plane.
material textMat pen textPen define style "3D" "Times New Roman", 300, 5, 0 style "3D" xform 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0 text 0.100, 0, textStr del 1
Figure 160 – Planar text.
Click the 3D View button to see how the changes have affected the model (figure 159). Zero Thickness Text By setting the thickness to zero, polygon-type characters can be created. Replace the 0.100 value in your 3D Script to zero, and again view the result (figure 160). Change the font to Arial, and add a simple 3D block behind the text to create our exit sign (figure 161). If we were to add a simple 2D Script and a user interface, this could become a handy Figure 161 – Add a block object. behind the text.
14 Working with Graphics Image files are used for Preview Pictures, for icons placed on the object’s Settings dialog, for graphic selection fields, for 3D cutout images, and for textures. In this chapter, we’ll take a brief look at how to create images. In particular we’ll consider images that include areas of transparency. Such images can be used to make ‘cardboard cut-out’ objects, or can be included in an object’s Settings dialog. We’ll also consider some ideas on the design of graphics for preview images and ‘icons’ for the user interface. Figure 162 – Original image.
14.1 File Types Before getting into the details of how to create images, I’d like to make an important note. Not all image file types are supported by ArchiCAD. Use .tif files for images with transparency. For other images, .jpg or .gif formats are OK, but use .gif or .tif files for precise pixel mapping. Of these, .gif files provide the same quality for a smaller file size.
14.2 Create an Image with Transparency
Figure 163 – Select parts of the image
Using Adobe Photoshop, you can create transparent areas in .tif files by defining an that are not a part of the subject. alpha channel. First, obtain the image (figure 162). For example, you might use an image from a digital camera. Images from other sources may be subject to copyright. In Photoshop, flatten the image so that it contains only one (background) layer. Use the Photoshop selection tools (magic wand, marquee, crop, etc.) to select the area you want to make transparent (figure 163). Delete the selected area (figure 164). In the Channels palette, create a new channel (figure 165). By default, it will be called Alpha 1 - that name is fine. Click to select the new channel (figure 166).
Figure 164 – Delete the selection.
184 Chapter 14: Working with Graphics - Images for the Object Settings Dialog Fill the selected area with black, then select the inverse and fill it with white. The result should be a white silhouette of the area you want to maintain (figure 167). The black area will become transparent. The background of the picture has now been removed, leaving just the parts you want to include in the user interface or cardboard cut-out. Finally, save the image as a .tif file, including transparency, into your library. The image is now ready to use in the Interface Script, or (in this case) a ‘cardboard cut- Figure 165 – Create an Alpha out’ object. channel.
14.3 Images for the Object Settings Dialog In the next chapter, we will take a look at the Interface Script. The Interface Script allows us to present an object’s parameters in a dialog, along with explanatory text and icons. It also allows us to present graphic selection fields that present settings to the as a palette of images. The images used in the Settings dialog can be either realistic images that represent the Figure 166 – Select the Alpha whole or part of the object, or simple, generic icons that indicate commonly Channel. recognized attributes such as pens, materials and the like. Designing images for a user interface is more art than science. There are no definitive answers as to what is right and what is wrong. In this section, I will outline some principles that I try to follow. Images and Transparency For any non-rectangular icon, a .tif image with a transparent alpha channel should be used (figures 168, 169). This provides a much tidier appearance, as the background of the Settings dialog trims neatly to the image. Figure 167 – Invert the original See the previous section of this chapter for instructions on creating a transparent selection, and fill the new selection with white. image. Image Size When designing a Settings dialog, I find it works best to use small icons rather than large images. This means you can use one icon per input field. The grouped input field plus icon forms a unit of script (and a unit on the dialog) that can be moved at
Chapter 14: Working with Graphics - Images for the Object Settings Dialog 185 will. The order of the infields can be changed, fields can be removed or new fields added to the dialog without having to change the images. In the next chapter, we will also see that the input fields can be moved from one interface page to another without difficulty. 17 This approach requires the creation of a relatively large number of icons, as each input field has its own icon. Actually this is not such a burden as you might think. Closely related fields (e.g. the width, thickness and spacing of decking boards) Figure 168 – Icons in an objects Settings often use nearly identical icons. Once you have made one of these icons, the others dialog – the white background should be can virtually be copies of the first with minor modifications. made transparent. To fit a reasonable number of fields onto the Settings dialog, the images must be quite small. I would recommend that they are no wider than 60 pixels, and no taller than 31 pixels. This allows you to arrange the icons + infields in a rectangular grid, with 4 rows and 5 columns. The aim is to use as few pixels as possible while still providing a clear and attractive image. Some classic examples of icons include Graphisoft’s material and pen icons. These icons are tiny, around 12 pixels in each direction, but easily recognized. Colors Use consistent colors for different materials e.g. brown for timber, grey for steel, yellow for molded plastic or fiberglass, deep green for glass, etc. Where 3D images are used, have consistent variations of the colors for front, side and top surfaces. Figure 169 – The white background has been exchanged for transparency via the For cylinders and spheres, use gradients. Don’t use the same color for every element. Graphisoft introduced the blue toolbox icons back in version 9 or 10. I personally now find it difficult to quickly find a roof (red) or a wall (gray), because everything is the same color! Admittedly,
Alpha channel.
17
When I first started creating user interfaces I used a single large image, with leader lines pointing to multiple fields arranged around the image. This approach was extremely difficult to work with. For one thing, it was difficult to align the input fields with the leader lines. Worse, if I wanted to add or remove an input field, or even change the order of the fields, I also had to edit the image. Another method that I’ve seen used (and tried myself) is similar, but instead of leader lines, dimensions are labeled A, B, C etc. Corresponding fields are listed beside the image and labeled to provide a link. This is OK but tends to be somewhat clumsy. Once again, adding and removing fields is a nightmare (although not quite as bad as the leader line method). The order of the fields also tends to be somewhat arbitrary.
186 Chapter 14: Working with Graphics - Images for the Object Settings Dialog I no longer use ArchiCAD for drafting – if I did I guess I would get used to it. My point is, when you have access to virtually unlimited colors, why restrict yourself to a single color? When Cadimage re-branded in 2010, we adopted a corporate colour scheme. As a result all of our interface icons are now created using a single color range. It looks good, it’s consistent, but perhaps we have lost something also. Consistency When a new user looks at your interface for the first time, she might not immediately understand the intended meaning of a particular icon. By reading the descriptive text or tooltip, editing the associated field, contacting your help desk, or reading a help article, she will learn what it represents. The next time she sees the same icon, she will assume that it means the same thing.
Figure 170 – The oblique
So it’s important that you use icons consistently. That way, people who use your software axonometry offers an easy and will quickly become familiar with their meanings. If you use a symbol for a pen setting in consistent way of drawing 3D in one object, use the exact same symbol in all objects. If you use a color to represent a Photoshop. timber material in one object, use the same color to represent timber in every other object. As well as being self-consistent, you should also try to be consistent with ArchiCAD’s icons. ArchiCAD uses perfectly good icons for line type, fill pens and the like. There’s no need to re-invent the wheel. 3D Images Use oblique projections for 3D images. Oblique projections allow you to draw the true shape of one face, with extensions running obliquely to indicate depth (figure 170). This method provides a consistent approach that also allows you to draw dimensions with tidy arrows. Identify surfaces pointing in different directions with different shades of the standard colors you have chosen. Slightly darken the side color, and lighten the top color. If you like, you can show shadows where this helps. Sometimes showing a shadow line Figure 171 – An image intended can help enhance the illusion of depth, and give an indication of how the various pieces for use as a selection list. This relate to each other. image includes 3 rows and 1 Dimension Lines The best way I’ve found to display dimension lines is:
column.
Chapter 14: Working with Graphics - Images for the Object Settings Dialog 187 •
Do not include witness lines unless necessary, in which case a line of dots is sufficient.
•
For horizontal and vertical dimensions, use a pyramid of pixels for the arrowhead, with a base of 5 and a height of 3 pixels.
•
For oblique dimensions, use a right triangle of pixels for the arrowhead, with a base of 3 and a height of 3 pixels.
•
Join the arrowheads with a dotted line if they are horizontal or vertical, otherwise a solid line.
•
For closely-spaced arrowheads, invert them and add a 2 pixel long shaft to each. Draw a dotted line between the arrowheads.
Images for Graphic Selection Fields When you create an image for graphic selection, you can include all the selections in a single image. The image is divided into a rectangular grid that may have any number of rows and columns (figure 171).
Figure 172 – Preview pictures at smallest size in the library browser.
Alternatively you can create an individual image for each option. Images for Preview Pictures The Preview Picture has a size of 128 x 128 pixels. The image is used as an icon in the library browser window, but the size of the icon is at best 64 x 64 pixels (figure 173) and can be as small as 16 x 16 pixels (figure 172). When the image is reduced to such a small size, any thin lines will most likely be removed. To prevent the image from disappearing or becoming unrecognizable, you should use areas of color rather than lines.
Figure 173 – Preview pictures at large size .
15 User Interface The set of tools that helps an end-user interact with an object is termed the user interface. User – object interactions include selection, referencing and editing. Selection and referencing are closely related. In normal use, objects are selected from a library, and placed into an ArchiCAD project. The location they are placed is often referenced relative to other objects or building elements. We need to provide handles that make it easy to reference key points and edges on our objects. Each object is ultimately controlled by adjusting the values of a list of parameters. There are various ways that the user can edit these values. The values could be typed into fields, selected from lists, or set graphically by adjusting lengths or angles. The goal of the interface is to provide the best method for the user to set each of the object’s parameters, in such a way that the user instinctively understands (or can at least guess at) how to do so. The smarter the user interface, the more simple and obvious it will appear. Like a clean window, the best interface is the one that is least noticed. The holy grail of interface design is to create an interface that requires no documentation. This does not mean that a new user will immediately appreciate the full extent of the object’s functionality. Rather, that a new user will be able to start using the object with understanding at a basic level, and will have confidence to explore its finer details as time goes by. The easier an object is to use, the more likely it will be used successfully and frequently. Consistency is key. Adopt a system for your user interfaces, and apply the system as consistently as possible across your whole library. This way, each object you produce will have a familiar look and feel. The user will recognize the basic structure, and will not have to learn new ways of achieving similar results. Like any other system, you should regularly reevaluate the way you design interfaces, so as to keep improving your methods. In this chapter, I will suggest some guidelines that I follow when designing user interfaces. These guidelines work well for me in ArchiCAD 14. As GDL evolves new interface tools will no doubt be introduced. As they emerge I will have to evaluate how they can help me to produce even better user interfaces.
15.1 Selection and Referencing Selection can include initial selection of an object from the library, or selection of a placed instance of an object in a drawing or model window. Both are used frequently during the course of an ArchiCAD project. Referencing is closely related to selection. When working in an ArchiCAD project, there will be times when you want to move one building element or object to touch or align to a specific point on another building element or object. Two goals for the GDL developer are to make objects that are easy to access from the library, and that (when placed into an ArchiCAD project) have handles at key points for selecting and referencing.
Chapter 15: User Interface - Selection and Referencing 189
15.1.1
Selection from the Library
When a user wants to select an object from a library, the object’s name, location, subtype and Preview Picture are important. The name should clearly indicate the intended use of the object. It should also be unique. Since ArchiCAD 11, long file names can be used, making it easier to ensure a unique name. For example, you can include the name of a manufacturer or your own company name to avoid conflicts with competing products or standard ArchiCAD libraries. It’s a good idea to adopt a consistent approach to naming objects. You can change the name of an object through a file browser such as Microsoft Windows Explorer. When you do so, all placed instances of the object will update to use the re-named version. The object’s subtype is important. As the user can search the library by subtype, you should be sure to choose the most appropriate subtype for each object you create. If an object is similar to one in the standard Graphisoft library, make sure you use the same subtype as the standard object. The Preview Picture will help the user select the correct object. For experienced users it provides visual recognition, and for new or occasional users it indicates the intended use of the object. For generic objects, it’s not quite as easy to indicate the full scope of an object, but you can at least indicate something about its intended or typical function. Remember that the Preview Picture is shown as a tiny icon in the library browser, so make sure it contains areas of color rather than fine lines which will disappear at small scales. The location of the object in the library is also important. Users want to be able to find an object quickly, which means that objects should be grouped logically, and that a limited number of objects or sub-folders should be presented in any one folder. On the other hand, too deep a folder structure can be just as annoying. I believe that you should never have more than three levels of nested folders in a library. This still allows for a vast number of folders. Let’s say that the main library contained 10 folders, each of which contained 10 sub-folders, each of which contained 10 objects. That’s 1000 objects, which is a mighty library.
15.1.2
Hotspots
Add hotspots to reference to strategic points on the object. Hotspots can be added in the 2D Script and the 3D Script. Every object should include at least one hotspot at its origin, and if there is to be any graphic editing, extra hotspots will be required. In many cases hotspots on the bounding box can be helpful. It is particularly important to provide hotspots at key points to help with initial placement. When an object is selected from the library, its Preview Picture displays all of the hotspots. The user is free to select any one of these hotspots as the placement anchor for the object. 2D hotspots should thus be provided at any point on the object which could sensibly be used as an anchor point. 18
18
When you place an object into an ArchiCAD project, you can choose which hotspot you want to use as the placement
190 Chapter 15: User Interface - Selection and Referencing To add a hotspot to the 2D symbol, use the hotspot2 command in the 2D Script. hotspot2 x, y
To add a hotspot to the 3D model, use the hotspot command in the 3D Script. hotspot x, y, z
15.1.3
2D Selection
In plan view, or for 2D objects placed in a detail or section window, one way to select an object is to click on a fill polygon that forms part of the object’s 2D symbol. No special scripting is required in this case. Simply script a fill polygon, and it will automatically provide a selection area. To reference an edge, include hotlines and hotarcs in the GDL 2D Script. These are intelligent selection edges that provide selection lines and arcs including specific points such as end points, mid points and divisions along the edges. The hotline2 command is identical to the line2 command, and the hotarc2 command is the same as the arc2 command. hotline2 x1, y1, x2, y2 hotarc2 center_x, center_y, radius, initial_angle, final_angle
Because hotlines and hotarcs are invisible, it’s not easy to know if you have created them in the right spot. To check, change the hotline2 to a line2 element. This will make the element visible, so that you can visually check if it is correct. Once you are sure its location is correct, you can change it back to a hotline2 element. You will want to provide a hotline for many of the lines in your 2D symbols. The reason for this is that people naturally click on a line when searching for a selection handle. Also, many of the important features of an object are drawn specifically as lines in the 2D symbol. A quick way to achieve this is to draw all the lines, then duplicate the GDL script for each line, and change it to a hotline2 element. When using the hotarc2 command, you should be aware that the angle must be less than 360°. To create a full circle, use two hotarc2 elements, one from 0° to 180°, and the other from 180° to 360°.
15.1.4
3D Model
In the 3D model, you can select an object by clicking anywhere on its surface. Hotspots can be added to reference important points. Selection can be made either via these hotspots (if you know where they are located), or else by clicking on any surface of the model. To create a hotspot in the 3D model, use the command: hotspot x_co-ordinate, y_co-ordinate, z_co-ordinate
point. By default, one or other hotspot is selected and will be the default placement point for that object. As a GDL programmer, you can control which hotspot is the default placement point. This is simply the first hotspot listed in your script.
Chapter 15: User Interface - Graphic Editing 191 Hotlines can also be added to the 3D model. hotline x_co-ordinate_1, y_co-ordinate_1, z_co-ordinate_1, x_co-ordinate_2, y_co-ordinate_2, z_co-ordinate_2, iHotline iHotline = iHotline + 1
The index variable iHotline should be initialized in the script header, and incremented each time a new hotline is added to the script (as in the template above).
15.1.5
Section/Elevation
All lines and fills in section/elevation view can be selected by a mouse click. The lines can all be referenced without any special scripting. All 3D hotspots will also appear in section and elevation views.
15.2 Graphic Editing Special dynamic hotspots can be added to provide graphic interaction. A user can move one hotspot relative to another to define a length or angle dimension which is fed back into a parameter value. They provide an intuitive way to adjust parameters.
15.2.1
A, B and ZZYZX Parameters
Every object has three parameters that define its bounding box – length, width and height. The variables for these parameters are A, B and ZZYZX. 19 These three parameters have special properties: •
•
19
When you place an object into the project, you can use various geometry methods. Two of these allow you to stretch the A and B dimensions as Figure 174 – Plan view hotspots separated by part of the placement process. a distance A in the x-direction, or B in the y-
Once an object has been placed into a project, any pair of hotspots direction, allow the user to graphically adjust separated by the value A in the direction of the object’s x-axis can be A and B. stretched apart to adjust the value of A. Similarly, any two hotspots separated by the value B in the direction of the object’s y-axis can be stretched to change the value of B.
Yes – A, B and C would have been a more logical choice. So why ZZYZX? This is one of the mysteries of GDL. All I can say for certain is that the vertical dimension parameter was added to GDL too late for C to be an option. We just have to accept ZZYZX and move on.
192 Chapter 15: User Interface - Graphic Editing •
In the 3D model, any pair of hotspots separated by the value of ZZYZX along the z-axis can be stretched apart to control the value of ZZYZX. A special option is provided in the pet palette to stretch the ZZYZX parameter.
The upshot of all this is that you can provide hotspots to control the values of the A, B and ZZYZX parameters. You should always use these values to define the characteristic size of your objects.
15.2.2
Dynamic Hotspots
Every dynamic hotspot includes the following parts: •
a position co-ordinate (x, y) or (x, y, z)
•
a unique index (i.e. unique to the script)
•
reference to a parameter via its variable name (this may be an element of an array variable)
•
a hotspot type index
15.2.3
Length Editing in the 2D Symbol
To dynamically edit the value of a length parameter, you must define 3 points – a reference point, a base point and a moving point. The base point is the point from which the length is to be measured. The moving point is the dynamic hotspot that the user will adjust. The parameter value will be adjusted to match the distance between the base point and the moving point. The reference point controls the direction of motion. Imagine an arrow with its tail on the reference point and its head on the base point. This defines the positive direction of measurement. It is important that you correctly define the positive direction of measurement for the dynamic hotspots. !Dynamic hotspot to adjust length_variable along the x-axis add2 reference_point_x, reference_point_y hotspot2 0, 0, hotspotIndex + 1, length_variable, 1 hotspot2 length_variable, 0, hotspotIndex + 2, length_variable, 2 hotspot2 -1, 0, hotspotIndex + 3, length_variable, 3 hotspotIndex = hotspotIndex + 3 del 1 !Dynamic hotspot to adjust length_variable along the y-axis add2 reference_point_x, reference_point_y hotspot2 0, 0, hotspotIndex + 1, length_variable, 1 hotspot2 0, length_variable, hotspotIndex + 2,
Chapter 15: User Interface - Graphic Editing 193 length_variable, 2 hotspot2 0, -1, hotspotIndex + 3, length_variable, 3 hotspotIndex = hotspotIndex + 3 del 1 !Dynamic hotspot to adjust length_variable along !a rotated co-ordinate rotation_angle add2 reference_point_x, reference_point_y rot2 rotation_angle hotspot2 0, 0, hotspotIndex + 1, length_variable, 1 hotspot2 length_variable, 0, hotspotIndex + 2, length_variable, 2 hotspot2 -1, 0, hotspotIndex + 3, length_variable, 3 hotspotIndex = hotspotIndex + 3
Figure 175 – A dynamic hotspot has been added to this wobbly bridge to adjust the value of the vertical droop.
del 2
15.2.4
Length Editing in 3D
Length editing in 3D is a natural extension to length editing in 2D. The only difference is that each of the three hotspots requires three co-ordinates, x, y and z. When using dynamic hotspots, the parameter value is displayed and can be edited using keyboard input (figure 175). !Dynamic hotspot to adjust length_variable along a vector vx, vy, vz add reference_point_x, reference_point_y, reference_point_z hotspot 0, 0, 0, hotspotIndex + 1, length_variable, 1 hotspot vx*length_variable, vy*length_variable, vz*length_variable, hotspotIndex + 2, length_variable, 2 hotspot -vx, -vy, -vz, hotspotIndex + 3, length_variable, 3 hotspotIndex = hotspotIndex + 3 del 1
15.2.5
Angle Editing in 2D
To edit an angle in the 2D symbol, you need to provide a center hotspot, a moving hotspot and a base hotspot. The following block of script defines a dynamic hotspot to adjust the value of an angle-type parameter with variable angle_variable. The hotspot is placed at a center point center_x, center_y. In this example, the angle is measured counter-clockwise from the x-axis. !Dynamic hotspot to adjust angle_variable counter-clockwise add2 center_x, center_y R = hotspot_radius
194 Chapter 15: User Interface - Graphic Editing hotspot2 0, 0, hotspotIndex + 1, angle_variable, 6
!Center hotspot
hotspot2 R*cos(angle_variable), R*sin(angle_variable), hotspotIndex + 2, angle_variable, 5 hotspot2 R, 0, hotspotIndex + 3, angle_variable, 4
!Moving hotspot !Base hotspot
hotspotIndex = hotspotIndex + 3 del 1
I would recommend using this script fragment for any angle. If you want to measure the angle from a direction other than the x-axis, just add a rot2 command after the add2. !Dynamic hotspot to adjust angle_variable counter-clockwise measured from zero_angle_direction add2 center_x, center_y rot2 zero_angle_direction R = hotspot_radius hotspot2 0, 0, hotspotIndex + 1, angle_variable, 6 hotspot2 R*cos(angle_variable), R*sin(angle_variable), hotspotIndex + 2, angle_variable, 5 hotspot2 R, 0, hotspotIndex + 3, angle_variable, 4 hotspotIndex = hotspotIndex + 3 del 2
To adjust the angle in a clockwise direction, add the value 512 to the center point and change the script as follows. !Dynamic hotspot to adjust angle_variable clockwise measured from zero_angle_direction add2 center_x, center_y rot2 zero_angle_direction R = hotspot_radius hotspot2 0, 0, hotspotIndex + 1, angle_variable, 6 + 512 hotspot2 R*cos(angle_variable), -R*sin(angle_variable), hotspotIndex + 2, angle_variable, 5 hotspot2 R, 0, hotspotIndex + 3, angle_variable, 4 hotspotIndex = hotspotIndex + 3 del 2
15.2.6
Angle Editing in 3D
To edit an angle in the 3D model, you need to provide a center hotspot, a moving hotspot, a base hotspot and a reference hotspot. The extra hotspot together with the center hotspot defines the axis of rotation. !Dynamic hotspot to adjust angle_variable counter-clockwise about the z-axis R = hotspot_radius
Chapter 15: User Interface - Graphic Editing 195 hotspot 0, 0, 1, hotspotIndex + 1, angle_variable, 7
!Reference hotspot
hotspot2 0, 0, hotspotIndex + 2, angle_variable, 6
!Center hotspot
hotspot2 R*cos(angle_variable), R*sin(angle_variable), 0, hotspotIndex + 3, angle_variable, 5
!Moving hotspot
hotspot2 R, 0, 0, hotspotIndex + 3, angle_variable, 4
!Base hotspot
hotspotIndex = hotspotIndex + 3
15.2.7
Hiding Hotspots
By default, the reference hotspots are invisible, and the base and center hotspots are visible. You can hide a base or a center hotspot by adding 128 to the mask value.
15.2.8
Coincident Hotspots
When two dynamic hotspots lie on top of each other, only one will be editable. To control which hotspot takes precedence in this situation, move the position of the controlled parameter in the Parameters list. Hotspots that control parameters at the top of the list will ‘float’ on top of hotspots that control parameters further down the list.
15.2.9
Edit Two Parameters Simultaneously
Especially in freeform modeling, you will want to provide hotspots capable of moving to any point in a given plane. This can be achieved by moving to the new location, defining a moving point at the new origin and base and reference points at a distance. It is not possible in ArchiCAD 14 to provide simultaneous dynamic editing in all 3 dimensions. It is also impossible to provide simultaneous angle and length editing. The following 2D Script allows hotspot adjustment in two dimensions. The 3D equivalent is similar. !Dynamic hotspot to adjust length_variable along the x-axis add2 reference_point_x + lengthVariable[1], reference_point_y + lengthVariable[2] !Adjust along the y-axis hotspot2 0, -lengthVariable[2], hotspotIndex + 1, length_variable[2], 1 hotspot2 0, 0, hotspotIndex + 2, length_variable[2], 2 hotspot2 0, -lengthVariable[2] - 1, hotspotIndex + 3, length_variable[2], 3 hotspotIndex = hotspotIndex + 3
196 Chapter 15: User Interface - Graphic Editing !Adjust along the x-axis hotspot2 -lengthVariable[1], 0, hotspotIndex + 1, length_variable[1], 1 hotspot2 0, 0, hotspotIndex + 2, length_variable[1], 2 hotspot2 -lengthVariable[1] - 1, 0, hotspotIndex + 3, length_variable[1], 3 hotspotIndex = hotspotIndex + 3 del 1
15.2.10 Edit Other Parameters based on the Latest Change Whenever you are moving a dynamic hotspot, the Parameter Script runs. Within the Parameter Script, you can adjust the values of other parameters in response to the dynamic adjustments. Use the global variable glob_modpar_name to find out which parameter that was adjusted. By also checking the value of glob_context you can test whether the user adjusted the parameter value in floor plan, 3D, or section/elevation views, or in the Settings dialog. At the head of the Parameter Script, add the following script fragment. This fragment substitutes the local variable modparName for the global variable glob_modpar_name. This substitution is required because of a quirk of the Parameter Script. The script runs multiple times following each edit event, and the value of glob_modpar_name remains the same each time the script runs. By capturing this value on the first run of the script, we can avoid repeated calculations. !Set variables buttonID and modparName to stand in for globals buttonID = 0 modparName = "" isFirst = 0 rrr = Application_Query ("Parameter_Script", "FirstOccasion_in_Progress", isFirst) if isFirst then buttonID = glob_ui_button_ID modparName = glob_modpar_name endif
Now you can use the value of modparName to identify which parameter was adjusted. if modparName =
length_variable_name then
other_parameter = expression parameters other_parameter = other_parameter endif
For example, the following script will check for a change in the parameter whose variable is boardWidth, and ensure that it always remains greater than or equal to boardThick.
Chapter 15: User Interface - Graphic Editing 197 !Ensure that the Board’s Width always exceeds the Thickness if modparName = “boardWidth” then if boardWidth < boardThick
then
boardWidth = boardThick parameters boardWidth = boardWidth endif endif
15.2.11 Edit One Parameter while Displaying Another Whenever a dynamic hotspot is adjusted, a palette appears displaying a parameter name and value. By default, the displayed parameter is the one directly set by the dynamic hotspot. It is, however, possible to display a different parameter name and value, and the parameter type may not necessarily be numeric. At the end of the hotspot2 command, you can add an optional value which is the name of the parameter you want to display. !Dynamic hotspot to adjust length_variable along the x-axis add2 reference_point_x, reference_point_y hotspot2 0, 0, hotspotIndex + 1, length_variable, 1 hotspot2 length_variable, 0, hotspotIndex + 2, length_variable, 2, other_parameter hotspot2 -1, 0, hotspotIndex + 3, length_variable, 3 hotspotIndex = hotspotIndex + 3 del 1
In the Parameter Script, you should add a block of script to link the two parameters. The Parameter Script can test for an adjustment to either one of the parameters by checking the value of glob_modpar_name. It can even check the context under which the parameter value was changed – was it edited via the object Settings dialog, or via a dynamic hotspot? If one of the parameters is edited, the other should be adjusted to keep them synchronized.
15.2.12 Use a Simplified Model When dynamically adjusting parameters, the 2D symbol or 3D (wire-frame) model re-builds whenever the mouse cursor is moved. If the model takes a long time to build, the result is a frustrating sense of un-responsiveness. The solution is to build a simplified model, or none at all, while moving the dynamic hotspots. In most cases, a simplified version is preferable, as the user gets graphic feedback on the result before clicking to apply them.
15.2.13 Rubber Banding Rather than re-building the whole model repeatedly as you stretch a dynamic hotspot, you may use a simplified model
198 Chapter 15: User Interface - Customizing the Settings Dialog while in dynamic editing mode. In some cases, this will mean that the effect of the stretch may not be visible until you exit the editing session. You may want to provide some ‘rubber bands’ to show the intended result of the stretch. For example, a hotspot at an edge center may intend to change the shape of a polygon edge from a straight edge to an arc. This technique is especially useful if you re-calculate the shape after adjusting the hotspot.
15.3 Customizing the Settings Dialog When you open an object’s Settings dialog, a Custom Settings dialog will appear below the Preview Picture. The object’s Interface Script allows you to add input fields, text, images, separators and buttons to this palette. The aim of the script is to provide a familiar environment in which users can edit parameters with confidence. In the Settings dialog, the various parameter fields must be presented in such a way that their function is clearly evident. Simplicity is the key. The slightest hint of ambiguity will lead to confusion.
15.3.1
Consistent Parameter Sets
One way to help users work with parameters is to use consistent parameter sets in each object you create. This allows the user to select a number of different objects, and set a parameter that applies to the whole selection set. For example, a library containing bathroom equipment might use a set of materials that applies to all the objects. Let’s say that there is a material parameter for the tap ware. Provided that the same variable is used in all the objects in your library, you can make a selection of all these objects, and choose a tap ware material. This material will be applied alike to the taps on your baths, vanities, showers, and to any stand-alone tap ware you may have placed. Batch changes like this can be achieved by using parameter variables that are common to all objects in the library. The object’s subtype will provide many of these common parameters. You can make your own template object to ensure that all necessary common parameters are automatically added to the objects in your library. For an example of this, see the Playground Equipment library.
15.3.2
Scripts that Control the Settings Dialog
The Interface Script and the Parameter Script combine to provide controls for the Settings dialog. Use the Interface Script to places infields, text, images and other controls onto the dialog. The user will see these controls when he/she opens the dialog for editing. Use the Parameter Script to assign values for any lists, to lock and unlock fields, and to control dependencies where the act of setting the value of one parameter or control will automatically change the values of other parameters. At a more advanced level, the Parameter Script can also be used to store sets of values into external files, and retrieve these values on request.
Chapter 15: User Interface - Customizing the Settings Dialog 199
15.3.3
Settings Dialog Name
By default, the customizeable part of the object’s Settings dialog is headed Custom Settings. To change the name of the palette, type the following GDL script at the top of the Interface Script window. !Syntax to set the Dialog Name ui_dialog dialog_name
Try it Yourself: Define a Name for a Dialog Create a new object, and click on the Interface Script Figure 176 – The Custom Settings dialog will remain locked until button. Copy the GDL script below into its Interface Script some interface elements have been added to the script. editing window. ui_dialog “My User Interface”
Close the object editing window, and save the changes you made to the object. Double-click on the Object tool in the ArchiCAD Toolbox. The Object Settings dialog will appear. You’ll see that the scriptable part of the Settings dialog has been re-named to My User Interface. If this is the only line of GDL in the object’s Interface Script, the customizable part of the Settings dialog will appear locked(figure 176). When you add script to place fields, text or graphics onto the palette, the dialog will become unlocked.
15.3.4
Dialog Size
The customizable part of the Settings dialog is a rectangular region of the object’s Settings dialog. Its width is 444 pixels wide, and its height 266 pixels. The dialog size is rather restrictive. As a result, most Figure 177 – The dialog co-ordinates are measured from the top left interfaces group the controls into smaller sub-sets, and of the Custom Settings area. include some mechanism (e.g. a menu) for selecting which group of controls to display.
200 Chapter 15: User Interface - Customizing the Settings Dialog
15.3.5
Pixel Co-ordinates
To refer to any pixel on the Settings dialog, use co-ordinates (x, y). The x-ordinate is measured from left to right, and the y-ordinate is measured from the top down. Thus, the top left pixel has co-ordinates (0, 0), and the bottom right pixel has co-ordinates (444, 266). Figure 177 shows some points on the Custom Settings dialog, with their co-ordinates.
15.3.6
Dialog Controls
Interface components (fields, text, buttons and graphics) can be placed anywhere on the customizable part of the Settings dialog. To place a component, you must type GDL commands into the Interface Script. The available commands are discussed below. Most interface components are given an (x, y) position on the customizable part of the Settings dialog, a width and a height. These values are always given in pixel co-ordinates as discussed in the preceding section.
15.3.7
Preview & Check the Dialog
While working in the Interface Script window, you can preview the Settings dialog at any time. Simply click the Preview button at the top of the Interface Script window (figure 178). The Preview window displays all the controls you defined in the Interface Script, apart from the dialog name. The Preview window also includes a Check button. Click on this button to check that all the controls all fit on the palette, with no overlap (figure 179). A report will be generated. Click on the OK button to return to the preview. Interface controls that overlap one another, or extend beyond the edge of the Settings dialog, will be highlighted. Figure 178 – Click on the Preview button to view the current Click on the OK button to return to the Interface Script. dialog elements. Edit the script to adjust the position or size of the control elements, so that they fit correctly on the dialog.
15.3.8
Text Style
You can define the text size and effects to use for interface components. To define the text style, use the ui_style
Chapter 15: User Interface - Images in the Settings Dialog 201 command: !Define Text Style ui_style text_size, effects
Text size is defined using an integer value: •
0 = regular size text
•
1 = large text
•
2 = small text
Text effects are defined as a second integer value: •
0 = regular text
•
1 = bold text
•
2 = italics
•
4 = underlined
Figure 179 – Click the Check button to view a report on overlapping items, and items that are outside the dialog area.
The text style does not apply to buttons or material, fill or line type infields. There are differences between the Macintosh and Windows platforms when it comes to text size. As a result I tend to use size infrequently.
15.4 Images in the Settings Dialog A picture is worth a thousand words. Architects and draftsmen tend to relate easily to graphical information.
Figure 180 – Overlapping interface
Where possible, you should include a visual cue in the settings dialog to identify elements are highlighted. the function of each control. These visual cues can be either realistic images that represent the whole or part of the object, or icons to indicate commonly recognized attributes such as pens, materials or the like. In most cases it is preferable to include both an image and a text label (or at least a tool tip) for the field. By identifying the function of the a control in two ways (graphical and textual), it’s less likely that the end-user will mis-understand it’s intended purpose. Where the shape of the image is not rectangular, tiff images with alpha channels should be used. This provides a much tidier appearance, as the background of the
202 Chapter 15: User Interface - Outfields Settings dialog trims neatly to the image. See Chapter 14 – Working with Graphics for details on how to produce an image with areas of transparency. When placing such an image onto the dialog, you must add an extra value to the ui_pict definition to indicate that it includes transparent regions. The definition becomes: !Place an Image onto the Settings Dialog ui_pict image_name, x, y, width, height, includes_transparency
The value for the includes_transparency flag should be set to either 0 (if not transparent) or 1 (if the image includes transparent areas). If you don’t specify a transparency flag, transparent parts of the image will be drawn opaque.
15.5 Outfields Text is useful for labeling fields, and communicating information. Use the ui_outfield command to add text to the user interface. This command has the following structure: !Place an Outfield ui_outfield text_to_display, x, y, width, height, alignment
The value of the text to display can be any GDL expression that returns a text value. The values of x, y, width and height are all expressed in pixels. The alignment is controlled by an integer: 0 =left aligned
1 = right aligned
2 = centered
4 = gray
Try it Yourself: Place Text on the Settings Dialog Create a new object and copy the following code into its Interface Script. ui_outfield “The quick brown fox jumps over the lazy dog”, 100, 50, 200, 15
Click on the Preview button to view the result (figure 181). The field width is too small for its contents. As a result, the text wraps to form a second Figure 181 – The rectangle line. However, the field height is too small to display the second line, so in the end only assigned to the outfield is not part of the text is displayed. To display the full contents of the outfield, we must adjust the large enough to contain the text. value of either the width or the height. Change the script to: ui_outfield “The quick brown fox jumps over the lazy dog”, 100, 50, 200, 30
Figure 182 – The text will wrap
Click on the Preview button to view the result (figure 182). You will see that the text has when it fills the rectangle. wrapped to form a second line.
Chapter 15: User Interface - Basic Input Fields 203
15.6 Basic Input Fields Much of the input data available in a typical Settings dialog is displayed using basic input Figure 183 – Length infields fields. These fields take raw data from the user, which may be either typed directly into the should be at least 60 px wide. field, or selected from a list of values. To display a field for the user to type a value for a parameter, use the ui_infield{2} command. !Add a Basic Input Field ui_infield{2} parameter_variable, x, y, width, height
Figure 184 – Angle infields
The parameter variable must be identical to one of the listed parameter variables of the should be at least 60 px wide. object.
The width and height of the infield control can take any (positive) pixel value. There are no hard-and fast rules for component size. However, here are some ‘rules of thumb’ that Figure 185 – Integer infields you might like to adopt. can be as few as 40 pixels wide. Length, Angle, Integer and Real infields should be 60 pixels wide by 19 pixels high. This width allows sufficient width for entering dimensions in feet and fractional inches. Boolean (checkbox) infields should be 18 pixels wide by 18 pixels high. This size works OK on both Macintosh and Windows platforms. If you use an outfield beside the checkbox, Figure 186 – Boolean infields clicking on the text will have no effect. To create a checkbox for which the text is ‘live’, use should be 18 x 18 px. the infield{3} command as explained in the next section. The width of Text infields depends on what type of data they will display. If the user is able to type custom text into the field, allow as much width as possible (I allow 180 pixels). If Figure 187 – The width of text the user can only select from a list of values, allow sufficient width to display the longest infields will depend on the string in the values list, with some extra space to allow for variations in platform. As with intended input. length infields, use a height of 19 pixels. For Material infields, allow a height of 22 pixels. The infield displays a sample of the RGB color, the material name, and icons to indicate whether a fill and/or a texture are associated Figure 188 – Make material with the material. Allow as much width as possible (I allow 180 pixels), so that the field infields as wide as possible. can display all this information. I also use a width of 180 pixels and a height of 22 pixels for Fill infields. For Line Type infields I use a width of 180 pixels, and a height of at least 27 pixels to allow Figure 189 – A fill infield of size 180 x 22 px. for the Preview Picture of the line.
204 Chapter 15: User Interface - Basic Input Fields Pen infields can be smaller – I use a width of 45 pixels and a height of 22 pixels. However, only the pen colour and a ‘sample’ line weight are displayed by the field – no pen index is shown. This is a problem for end users, as they can’t see at a glance which pen is currently Figure 190 – A linetype infield. selected. To overcome this problem I display the pen index as an outfield beside the pen selection infield, as in the sample script fragment below: !Contour Pen ui_infield{2} gs_cont_pen, uiX, uiY, 45, 19 ui_outfield “[” + str(gs_cont_pen, 1, 0) + “]”, uiX + 50, uiY + 3, 40, 14
Figure 191 – A pen infield can be relatively small, say 45 x 19.
For all field types, smaller widths and heights than recommended above may be used. However, the effect is not usually very acceptable, and it is better to use the dimensions given above. Elements of an array parameter can be displayed individually. For example, the script: !Input a single element of an array ui_infield{2} panelType[3][2], 100, 40, 80, 19
will place an infield onto the interface palette. The value controlled by the infield will be the element that occupies the 3rd row and the 2nd column of the array parameter sashType.
15.6.1
Boolean Input Fields
It’s quite common to see checkboxes on a dialog, with text to the right. If you click on either the check-box or on the associated text, you expect the checkbox to update. However if you place a Boolean (check-box) input field as described in the preceding section, clicking on the text (an outfield) will have no effect. To place a checkbox that has ‘live’ text beside it, use the ui_infield{3} command as follows: !Syntax to place a Checkbox with ‘Live’ Text ui_infield{3} parameter_variable, uiX, uiY, 180, 18, 6, “”, 0, 0, 18, 18, 18, 18, “”, text_to_display, 0, “”, text_to_display, 1
15.6.2
Use uiX and uiY
Nowadays I tend to arrange each Settings dialog as a rectangular grid, with an infield and icon in each cell of the grid.
Chapter 15: User Interface - Values Lists & Graphic Infields 205 Each input field occupies a separate ‘row’ and ‘column’ of the grid. The image is placed above the input field. I use integer variables uiX and uiY for the (x, y) co-ordinates of the input field. The image is placed at (x + dx, y – dy) where the values of dx and dy are chosen to center the image a minimum of 4 pixels above the input field. After placing each icon & infield, I increase the value of uiX. When I reach the right-hand edge of the Settings dialog, I increase the value of uiY and reset the value of uiX to the position of the first column. Each ‘cell’ of the grid thus forms a single, commented block of code in the Interface Script. Using the variable uiX and uiY makes it easy to change the relative positions of two input fields (cells), or insert or delete an input field. For parts of the dialog that include fields for pen, material, fill or line type selection, I use a variation of the grid arrangement. Owing to the width of material, fill and line type input fields, it doesn’t make sense to include multiple columns. Instead, I place each field on a separate row, labeling it with an outfield label to the right. The label height usually differs from the input field height. To keep the label and the field visually aligned, you will have to offset the label position down
15.7 Values Lists & Graphic Infields In the preceding section we saw how to add input fields to the Settings dialog. In this section we’ll look at how values lists can be added to these fields. We’ll also take a look at graphic selection input fields, in which the user is presented with a number of options, each of which has an image and a label.
15.7.1
Values Lists
In many cases, you will want the user to select a value from a list, rather than type into a field. For example, you might have a limited list of shapes for a cabinet (e.g. “Rectangular” and “Corner”). The 2D Script and 3D Script are programmed to recognize the names of these shapes, and to model the cabinet differently depending on which is selected. If a user were to enter a name that the scripts did not recognize (e.g. “corner” or “Straight”), the object would fail to generate a 2D symbol or a 3D model. Rather than have the user type in the name of a cabinet, we can provide a list of all the available options. This will ensure that a valid option is selected. To define a list of values for an input field, use the values command in the Parameter Script. !Syntax for Setting Values values “parameter_name” value_1,
206 Chapter 15: User Interface - Values Lists & Graphic Infields value_2, value_3, . . . value_n
In some cases, you might want to allow the user to enter a custom value that is not in the values list. This situation might occur, for example, when using a label. Standard options for the label may be to display the user ID of the object, its size, or the name of the associated zone. These items may appear in a list. However, it is also quite likely that the user will want to enter a custom text for the label. To allow the user to type into a field that has list values, append the keyword custom to the end of the values list. !Syntax for including a Custom Value values “parameter_name” value_1, value_2, value_3, . . . value_n, custom
15.7.2
Basic Selection Field
If you defined a values list for a parameter, the end user will see a small arrow button to the right of the input field. By clicking on that arrow, a list of the available values will appear. This requires no extra scripting in the Interface Script.
15.7.3
Graphic Selections
A couple of options are available when creating graphic selection fields. I find the ui_infield{3} command gives the best level of control. ui_infield{3} parameter_variable, x, y, width, height, field_type, image_file, n_cells, n_rows, selection_width, selection_height, cell_width, cell_height, cell_index_1, descriptive_text_1, parameter_value_1, . . . cell_index_n, descriptive_text_n, parameter_value_n
The ui_infield{3} element can be used to provide graphic selection from a palette, a list, a set of radio buttons or an expanding icon. Use the field_type variable to control which of these options you want to use.
Chapter 15: User Interface - Values Lists & Graphic Infields 207 Field Type To present a palette of options with descriptive text, set the field_type value to 1 (figure 192). To calculate the selection_height, take the image cell_height and add 18 pixels for each line of text that will appear below the selection graphic. In the example shown, we are Figure 192 – Field type 1 allowing for two lines of text. To calculate the width of the infield, multiply the number of desired columns by selection_width then add an extra 25 pixels to allow for the scroll-bar. To calculate the height of the infield, multiply the desired number of visible rows by the selection_height, and add 5 pixels for the field borders.
presents a palette of options.
To display a pop-up menu, set the field_type value to 2 (figure 193). You can display the image only, or include the text. When a user clicks on the field, all the images and text will appear in a list. It’s best to use images with transparency. When designing the images, include a small margin (say 3 pixels or so) around each image. This prevents the text getting too close to the images, and provides a tolerance which can be useful when designing interfaces that should work on different platforms.
Figure 193 – Field type 2
For a popup icon radio control set the field_type to 3 (figure 194). This is a good option presents a menu of options. if you are short of space and have a relatively few items in the list. The options are
displayed on a single row, and no text is displayed. When placed on a Custom Settings palette, it is not easy to distinguish this type of infield from a regular image, so use it with care. Make the field 5 or 6 pixels taller than the image cell_height, and perhaps 10 pixels wider then cell_width. I normally avoid using infield type 3, as it is easy for an Figure 194 – Field type 3 preend-user to mis-interpret this as a static image. sents a pop-up icon radio control. A variation of the popup icon radio control is the icon radio control (field type 4). In this case, all options are visible at all times. The options are placed in a single row, and each selection cell is automatically calculated to fit within the width of the infield (figure 195). Image File There are two options for the image source. The first option is that all images in the Figure 195 – Field type 4 graphic selection are sourced from either a single image file. The second option is that each presents an icon radio control. image is a separate file. In either case, the images must be included in the loaded library or
208 Chapter 15: User Interface - Values Lists & Graphic Infields they will not show up. If you choose to use a single image file, set it up as a grid of sub-images. The number of rows of images (n_rows) and the total number of cells (n_cells) in the grid are given as arguments of the ui_infield command. Cells in the image are defined by these two values. The image is automatically split into a rectangular grid with n_rows rows, and in each row enough columns to provide a minimum of n_cells cells. ui_infield{3} parameter_variable, x, y, width, height, field_type, image_file, n_cells, n_rows, selection_width, selection_height, cell_width, cell_height, cell_index_1, descriptive_text_1, parameter_value_1, . . . cell_index_n, descriptive_text_n, parameter_value_n
In the second case, set the image_file to the empty string “”, and set both the number of rows and cells to zero. Instead of the cell index, state the image file name to use for each option. ui_infield{3} parameter_variable, x, y, width, height, field_type, “”, 0, 0, selection_width, selection_height, cell_width, cell_height, image_file_1, descriptive_text_1, parameter_value_1, . . . image_file_n, descriptive_text_n, parameter_value_n
Cell Index If you are using a single image file for all the selection options, the cell_index values are an index to the image cell. Imagine your image divided into a grid of n_rows rows, each containing n_cols columns, where nRows × nCols = nCells . Each grid cell is treated as a separate image that can be displayed on the selection field. To display a particular cell of the image, enter its index in the cell_index slot. The indexing starts at 1, the top, left cell. Cell index 2 is the next cell to the right, and so on to cell index m which is the top, right cell. The next index is the leftmost cell on the second row, and so on to the last index which is at the bottom, right cell (figure 196).
Figure 196 – A single image used for a selection-type input field is broken into cells. These are indexed by Note that the cell_index value must not exceed the stated number of cells. row and column as If you like, you can use a separate image for each selection. In this case, type the file name in the illustrated. cell_index slot. There is no need to include the file extension.
Chapter 15: User Interface - Values Lists & Graphic Infields 209 Descriptive Text For each displayed list item, you need to include a descriptive_text. This text is displayed below the palette type infield (field_type = 1) and beside the pop-up menu infield (field_type = 2). For icon radio buttons, the text does not appear at all. The text should be concise to use as little space as possible. It is there to confirms the user’s interpretation of the icon. Parameter Value Each choice displayed in the selection infield requires three parts – the cell_index, the descriptive_text and the parameter_value. We have discussed the first two parts above. The parameter_value must match a value listed in the Parameter Script. If no match is found, that item will not appear in the list. Try it Yourself: Create a Graphic Selection Infield To see how this works, create a new object with a text-type parameter sidingType. Create a tiff image named “Siding_Type_img.tif” with 10 cells in 2 rows. Make each cell 40 x 45 so that the whole image is 200 x 90 pixels. In each cell draw an Figure 197 – An image for use by a image of a cladding type. Save the image into your library, and re-load the library selection infield. into your ArchiCAD project. Copy the following script into the Parameter Script editing window. !Siding Type Values values "sidingType" "Generic", "Bricks", "Weatherboards", "Board and Battens", "Rusticated", "Panels", "Corrugate", "Logs", "Vinyl Weatherboards", "Ribbed"
Copy the following script into the Interface Script editing window (note that you should copy the full list – I’ve used ‘. . . ‘ to save paper). !Initialize uiX, uiY uiX0 = 90
210 Chapter 15: User Interface - Values Lists & Graphic Infields uiY0 = 0 uiX = uiX0 uiY = uiY0 !Palette ui_outfield "Type 1", 0, uiY + 5, uiX0 - 5, 14 ui_infield{3} sidingType, uiX, uiY, 80*3 + 23, 70*2 + 5, 1, "Siding_Type_img", 10, 2, 80, 70, 40, 45, 1, "Generic", "Generic", 2, "Bricks / Blocks", "Bricks", 3, "Weatherboards", "Weatherboards", 4, "Board & Battens", "Board and Battens", 5, "Shiplap", "Rusticated", 6, "Panels", "Panels", 7, "Corrugate", "Corrugate", 8, "Logs", "Logs", 9, "Vinyl Weatherboards", "Vinyl Weatherboards", 10, "Ribbed", "Ribbed" uiY = uiY + 70*2 + 5 + 2 !Pop-up Menu ui_outfield "Type 2", 0, uiY + 5, uiX0 - 5, 14 !Image Only ui_infield{3} sidingType, uiX, uiY, 50, 57, 2, "Siding_Type_img", 10, 2, 40, 45, 40, 45, 1, "Generic", "Generic", 2, "Bricks / Blocks", "Bricks", 3, "Weatherboards", "Weatherboards", 4, "Board & Battens", "Board and Battens", 5, "Shiplap", "Rusticated", 6, "Panels", "Panels", 7, "Corrugate", "Corrugate", 8, "Logs", "Logs", 9, "Vinyl Weatherboards", "Vinyl Weatherboards", 10, "Ribbed", "Ribbed" uiX = uiX + 60
Chapter 15: User Interface - Values Lists & Graphic Infields 211 !Showing Text ui_infield{3} sidingType, uiX, uiY, 203, 57, 2, "Siding_Type_img", 10, 2, 40, 45, 40, 45, 1, "Generic", "Generic", 2, "Bricks / Blocks", "Bricks", . . . 10, "Ribbed", "Ribbed" uiX = uiX0 uiY = uiY + 59 !Pop-up Icon Radio Control ui_outfield "Type 3, 4", 0, uiY + 5, uiX0 - 5, 14 ui_infield{3} sidingType, uiX, uiY, 50, 60, 3, "Siding_Type_img", 10, 2, 40, 45, 40, 45, 1, "Generic", "Generic", 2, "Bricks / Blocks", "Bricks", . . . 10, "Ribbed", "Ribbed" uiX = uiX + 60 !Icon Radio Control ui_infield{3} sidingType, uiX, uiY, 45*6, 60, 4, "Siding_Type_img", 10, 2, 40, 45, 40, 45, 1, "Generic", "Generic", 2, "Bricks / Blocks", "Bricks", . . . 6, "Panels", "Panels" !
7, "Corrugate", "Corrugate",
!
8, "Logs", "Logs",
!
9, "Vinyl Weatherboards", "Vinyl Weatherboards",
!
10, "Ribbed", "Ribbed"
Figure 198 – Various types of selection infields.
Click on the Preview button at the top of the Interface Script editing window to view the result (figure 198). Try using the infields to select a cladding type, so as to get a feel for how they work. Note that by using the ui_infield{3} command, we can display any descriptive text on the infields. It need not match
212 Chapter 15: User Interface - Buttons the values listed in the Parameter Script (for instance we used Board & Battens in the user interface but Board and Battens in the Parameter Script). This is extremely useful when working with different languages – variables can be used to display the translated descriptive text for any given language without having to change the Parameter Script.
15.8 Buttons Simple button elements can be added to the Settings dialog. These provide a level of interaction without directly setting a parameter value. There are two useful types of button – the function button and the link button. Function buttons prompt the Parameter Script to run. On running, the Parameter Script ‘knows’ which button was clicked – the button’s ID is stored in the global variable glob_ui_button_ID. Thus you can add commands to the Parameter Script that run only if a certain button has been clicked. Link buttons open the specified URL. They may (for example) link to documentation or a help page on your web site.
15.8.1
Function Buttons
To place a function button into the Settings dialog, use the ui_button command as shown below. !Place a Function Button ui_button ui_function, button_name, x, y, width, height, button_ID
The button_name value is the text that will appear on the button. The button_ID value is an integer that uniquely defines the button. You can use any integer (including negative integers) for the ID. When a user clicks on the button, the value of global variable glob_ui_button_ID is set to match the value of button_ID, and the Parameter Script runs. If no button was clicked, the value of glob_ui_button_ID is set to zero. In the Parameter Script, you can use the value of glob_ui_button_ID to check which button was clicked. Based on which (if any) button was clicked, you can script a function to be carried out.
15.8.2
Make a Function Button Work
When you place a function button, part of the definition is an integer ID. When a user clicks on the button, the value of the global parameter glob_ui_button_ID is set to match this ID, and the Parameter Script runs. In the Parameter Script, then, you can test for a particular button-click event by checking the current value of glob_ui_button_ID. When this value matches the ID of a given button, the Parameter Script can be coded to respond to the user click. One odd feature of the Parameter Script is that it runs multiple times, and the value of glob_ui_button_ID is not re-set
Chapter 15: User Interface - Buttons 213 to zero automatically. This is a real problem. Say, for instance, that you add a button with ID 13, whose purpose is to increment the value of a parameter. Each time the uses clicks the button, the value of the parameter should increase by 1. In practice, this is impossible. When the user clicks on the button, the value of glob_ui_button_ID is set to 13, and the Parameter Script runs. In the Parameter Script, a line of code checks for the event that glob_ui_button_ID = 13, and in this case of course the condition is met, so the script proceeds to increment the parameter. So far everything has worked as it should. But then comes the snag – the Parameter Script runs for a second time! Because the value of glob_ui_button_ID is still set to 13, the condition is met once more and the parameter value is incremented for a second time. So for a single button click, the parameter value is incremented twice. To avoid this type of error, it is important to force the Parameter Script to respond only once to a button-click event. Add the following lines at the head of the Parameter Script. !Set variables buttonID and modparName to stand in for globals buttonID = 0 modparName = "" isFirst = 0 rrr = Application_Query ("Parameter_Script", "FirstOccasion_in_Progress", isFirst) if isFirst then buttonID = glob_ui_button_ID modparName = glob_modpar_name endif
This script fragment sets the values of variables modparName and buttonID to match those of glob_modpar_name and glob_ui_button_ID the first time the Parameter Script runs. For all subsequent runs the values of these variables are set to the empty string and zero. Rather than using the global variable glob_ui_button_ID to check if the button has been clicked, use the local variable buttonID instead. In practice, this looks something like the script fragment below: !Check if button with ID 13 has been clicked if buttonID = 13 then … do something … endif
15.8.3
Link Buttons
To place a link button, specify the position and size of the button, the text to display, and the URL of the link file as
214 Chapter 15: User Interface - Tool Tips shown below. ui_button ui_link, button_name, uiX, uiY, button_width, button_height, 0, “http://www...”
For example, each Cadimage library part includes a link button to a relevant online help article.
15.8.4
Picture Buttons
Regular buttons display text, but an alternative is to display an icon on a button. Use a .tif file with transparency. The file must be included in the loaded Figure 199 - The Cadimage Wall Covering library part includes a library. picture button that links to a relevant online help article. ui_pict_button ui_function, picture_filename, button_name, uiX, uiY, button_width, button_height, button_ID
The image is placed centrally on the button at full size (i.e. the image is not scaled to fit the button).
Figure 200 – Use a tooltip to provide information about the intended
Both link and function buttons may be placed as picture use of an infield. buttons (figure 199).
15.9 Tool Tips Tool tips are messages that pop up on moving the mouse cursor over particular interface elements (figure 200). I now use tool tips in preference to labels. They take up no room on the Settings dialog, and can fit more text than an outfield. Their role is to provide a description of what the associated control is supposed to do. Provided the icon is clear, they should be seldom required. A tool tip can be attached to any interface element. Simply add the keyword ui_tool_tip followed by the text you want to display. For instance: ui_infield{2} parameter_name, x_position, y_position, field_width, field_height ui_tooltip text_to_display
Just be aware that tool tips on the Windows platform cannot be multi-lined. All text appears in a single line. As a result, tool tips should not be too wordy.
Chapter 15: User Interface - Organizing Data 215 To see how tool tips work, create a new object. Copy the following script into its Interface Script editing window. ui_infield “A”, 0, 0, 80, 19 ui_tooltip “Total length of the object.” ui_outfield “Length”, 85, 3, 200, 15
Save the object as Tool Tip into your example library. Select the Tool Tip object in the library browser, and click on the Custom Settings tab. You will see an input field and some text. Hold the mouse cursor over the infield to display the tool tip.
15.10
Organizing Data
A GDL object may possess numerous (perhaps hundreds) of parameters. In such a case, a single palette of input fields is too limited to display all of the controls. It is necessary to group the controls into related subsets, and to display each subset separately. This leads to the concept of pages of controls. Navigation between pages can be achieved using a menu of some kind. On each page, the controls can be further grouped using separators and groupboxes.
15.10.1 Grouped Fields Where necessary, organize the interface palette using group boxes and separators. Use group boxes to group related fields, and separators to separate unrelated fields. A group box is a rectangular outline with a title. To place a group box onto the Settings dialog, use the following GDL code: ui_groupbox box_name, x, y, width, height
where the value for name can be any GDL expression that returns a text value, and x, y, width and height are given in pixels. A separator is a vertical or horizontal line drawn on the interface. It has no title. To place a separator line onto the user interface palette, use the following GDL code. ui_separator left_x, top_y, right_x, bottom_y
This interface component is unique in that it does not use a width or height in its definition.
15.10.2 Pages and Menus Even rather simple GDL objects can soon accumulate a wealth of parameters. A typical object might include parameters to define the 3D form, parameters to select materials, parameters to set pens and fill types, and parameters to define text to display as a label. If the 3D form is made up of more than one component, each component will have its own set of parameters. It would be physically impossible to fit all of these parameters onto the 640 x 400 pixels of the Settings dialog. It would also be extremely daunting for a user to be faced with, say, 100 or more parameters on opening the dialog.
216 Chapter 15: User Interface - Organizing Data So as not to frighten the end user, and in order to fit all the parameters onto the dialog, it is necessary to display only a small subset of parameters at any one time. This could be achieved using a wizard, where subsets of parameters are presented in a pre-defined order. I guess this approach would be OK if there were only two or three subsets of parameters to edit. I personally prefer a menu system, where the parameter subsets are accessed via a list of page titles. This approach provides greater freedom, as the end user can navigate directly to any page at any time.
Figure 201 – You can produce a menu
One way to display a menu is as a graphic infield. The graphic is basically a picture using a graphic selection field, where the of the menu text, with icons to indicate whether a page contains parts and, if so, images display the page names. whether it is open or closed (figure 201). In the Parameter Script, add the following script. !Pages values “page” 1, 2, 3
In the Interface Script, copy the script below to display the menu. !Page menu ui_infield{3} “page”, 0, 0, 120, 180, 2, “menu_image”, 3, 3, 100, 20, 100, 20, 1, “1”, 1,
Figure 202 – By clicking in the graphic selection, you are in fact choosing a page index.
2, “2”, 2, 3, “3”, 3
Of course, you will need an image for the menu to work. Match the image size to the size specified by the infield element – in this case 100 pixels wide and 60 pixels high. If you have more than 3 pages, just add the extra page numbers in the Parameter Script and in the menu infield.
15.10.3 Choosing Logical Parameter Subsets A good rule of thumb when choosing subsets of parameters is to think of how the object will be used. Usually there are a bunch of settings that define the overall structure, and other settings that concentrate on specific components. There are also materials, pens, line types and fills. During the concept design phase, an architect is probably most interested in the big picture. Chances are, he won’t pay much attention to the exact dimensions of the smaller components, so these will likely retain their default settings. Materials and pens will probably be left with their default settings too. When the documentation phase starts up, closer attention will probably be paid to the smaller scale details. At this point,
Chapter 15: User Interface - Interactive Parameters 217 the designer may focus on a particular component, and set its dimensions. Pens, fills and linetypes will be important, too, as they must conform to the office standards. For presentation renderings, material selections may become important. By thinking of the way the object may be used, we can develop a strategy for grouping parameters. On the first page, we’ll provide controls for the overall shape and size of the object. Subsequent pages will display settings to control individual components of the object. After this, we will add a page that includes all of the object’s materials. The last page will include settings for pens, linetypes and fills. For more simple objects, we may be able to combine materials, pens and line types on a single page, or even combine all the parameters on a one-page interface. However, the order of the fields should remain the same to give a familiar feel to the palette.
15.10.4 Displaying Parameter Sets The final step in making your menu structure work is to create a series of instructions within the Parameter and Interface Scripts that will display the correct content for the selected page of the user interface palette. The simplest way to do this is to apply a series of if – then statements for each page. Within the if – then statement for a page, you can create a series of ‘if – then’ statements for each part. !Fields for page 1 if page = 1 then … display the content of page 1 … Endif !Fields for page 2 if page = 2 then … display the content of page 2 … endif etc.
Inside each ‘if – then’ statement, you can place the commands that control the content of the specified page and part.
15.11
Interactive Parameters
One function of the Parameter Script is to control the stored values of variables in the parameter list. It does so by automatically altering parameter values in response to a user input. This is frequently used for error handling. It is easy, when using a GDL object, to enter erroneous data. Without some way of identifying user errors, and rectifying them, severe problems can occur. Using the Parameter Script, we can identify unreasonable values (often zero, negative, or very
218 Chapter 15: User Interface - Interactive Parameters large values), and re-set them or the values of related parameters to maintain the integrity of the object. Button clicks can also result in parameter values being set. The global glob_ui_buttonID value is used to detect a button click and the Parameter Script can run a function in response to the click event. While the Parameter Script is an essential part of a GDL object, it also presents some major challenges. •
Parameter values that are set via the script apply to all currently selected objects. This can cause bizarre results that cannot be easily avoided.
•
Run-time variable values cannot be set directly via macros. The values must be returned to the main script.
•
Debugging is extremely difficult. The print statement does not work when the object is running the Parameter Script, so run-time feedback is not possible.
•
The script runs multiple times. This can in some cases cause severe loss of performance, and can add to the frustrations of debugging.
15.11.1 Setting Parameter Values To set the values of a parameter, or of multiple parameters, use the parameters command. parameters variable_1 = value_1, variable_2 = value_2, . . . variable_n = value_n
15.11.2 Working with Selections of Multiple Objects Editing multiple selections of objects can pose problems. When a user sets a parameter, or clicks a button, the Parameter Script will run, possibly setting the values of other parameters. Of course, two or more objects in the selection set may share a common parameter. If this parameter is set by the Parameter Script, it will take the same exact value in all the objects that share it. Imagine a collection of 3D trees. Each tree has the same parameter set, but the precise form of each tree is unique as it is randomized and possibly of differing species. A user places some trees into his projecet. He then selects some of the placed trees and changes a parameter value. This causes the Parameter Script to run, which in turn changes the parameters that control the tree form. The result is that all the trees end up looking identical. There are ways you can reduce the risk of this effect. For one thing, you can choose to set certain parameters only in specific situations, such as when a particular button is clicked, or when a specific parameter value is changed. In our tree example, the tree’s form need not be re-calculated if a material parameter is edited. But try as you may, there is no way to completely avoid the problem. Even if you use different variable names for each
Chapter 15: User Interface - Interactive Parameters 219 object you create (which goes against the concept of subtypes and inherited parameters), the randomized trees of our example still run the risk of looking like clones.
15.11.3 Debugging the Parameter Script Debugging the Parameter Script is quite a challenge, as it is impossible to examine the values of variables as the script runs. The best that can be done is to output values to a text file as the script runs, then examine the file to see what was output. To output data to a text file, use the following script fragment. !Output Variable Values while the Parameter Script is Running debugChannel = open(“text”, “debug.txt”, “mode = WA”) output debugChannel, 1, 0, data_1, data_2, data_3, …, data_n close debugChannel
In the above script, the output data may be either numeric or text, and may include variables and constants. To get the output to work, you’ll need permission to write to files within the ArchiCAD Data Folder. It’s a good idea to include in the output, the name of the object or macro that is performing the output. The reason for this is that if you forget to delete the output script, the text file will get more data written to it each time the script runs. This could become problematic. And there’s no easy way of knowing where the data is coming from.
16 Approximating Curves There will be times when you want to model a curved line or surface that cannot be constructed directly using the standard elements. At least for the 3D model, and to a lesser extent for the 2D symbol, you can overcome the limitations of the standard elements by approximating the curves with a set of line segments. In 3D this provides a perfect solution. Every curved 3D surface is constructed using a mesh of small planar facets. You can clearly see this mesh if you set the 3D Window Settings > Mode to Wireframe. In 2D, this approximation approach has one major shortcoming. The only available ‘hot’ elements are hotlines and hotarcs. As a result, it’s difficult to provide reference edges for anything other than lines and arcs. We have to accept this problem, but hey, at least we can trace the curve. In chapter 21 we will design some re-usable sub-routines to perform polygon operations. Our polygons will include straight edges exclusively. To apply the sub-routines to polygons that include Figure 203 – This curves, we must first break the curves into sets of straight edges. This fits well with our approach of wineglass was modusing short line segments to approximate curves. eled using approxIn this chapter, we’ll look at how to approximate arcs, ellipses and helices.
imations to a series of Bezier splines.
We’ll then go on to consider two families of mathematical splines that are commonly used to fit curves through data points. The family of regular cubic splines includes closed, clamped and natural cubic splines. These functions, given a set of data points, will automatically fit a curve to the data. This is really handy in some situations, where the exact path of the curve is not critical. The Bezier spline family of curves is widely used for freeform modeling. It owes its success to a number of factors. Bezier splines are easy to calculate, and take relatively few processing steps compared to traditional cubic splines. They are defined piecewise, with each piece being controlled independently from its neighbors. This makes them easy to control compared to the regular cubic splines, which tend to have a mind of their own.
16.1 Circular Arcs
Figure 204 – The
A circular arc is defined by a center point, a radius, a start angle and an end angle. The distance curved surface is between the center point and any point on the circumference is equal to the constant radius R. It’s approximated using easy to see how an arc can be approximated using a set of short line segments. Each line segment is planar polygons.
Chapter 16: Approximating Curves - Circular Arcs 221 of equal length, and subtends a small angle φ (figure 205). If we draw a line segment starting at the center of the circle at angle θ , its end must lie on the circumference of the circle. The x and y co-ordinates are R cos θ and R sin θ . To draw our arc using line segments, then, we will iteratively calculate the start of each line segment, from angle α to angle β using step size φ (figure 205). The only remaining problem, then, is to choose a sensible step size φ. This is a matter of choice, but here’s one option you might like to consider. First, we’ll make the arbitrary decision that an arc must contain at least one point for every 45 degrees. Figure 205 – An arc can be approxnSegmentsMin = int(1 + abs(q2 –q1)/45)
imated using short line segments.
Next, we’ll use something like the resol command to ensure that the deviation between the arc and its approximating line segments never gets larger than 1mm. dq = acs((R – 0.001)/R) nSegments = max(nSegmentsMin, int(180/dq))
Having calculated the number of segments that the arc should have, we can calculate the angle for each line segment (figure 206). dq = (q2 – q1)/nSegments
Finally we can draw the arc. for qi = q1 to q2 + dq/2 step dq put R*cos(qi), R*sin(qi) next qi poly2 nsp/2, 1, get(nsp)
Try it Yourself: Approximate an Arc using Line Segments To see how this works, create a new object with angle parameters q1 and q2, and length parameters R and dR. Set the angle values to represent the start and end point of an arc, and the value of R to an arc radius. Set a value for dR to represent the maximum allowed deviation between the true arc and the approximating lines. Copy the following GDL script into the object’s 2D Script editing window.
Figure 206 – Given the arc radius and an angle Ɵ we can calculate the (x, y) coordinates of a point on the arc.
222 Chapter 16: Approximating Curves - Circular Arcs tol = 0.00001 !Draw an approximation to a circular arc getValues = 1 put R, q1, q2, dR gosub "21 Approximate Arc" poly2 nsp/2, 1, get(nsp) end "21 Approximate Arc": !Input: ! – radius: R~21 ! – start and end angles: q1~21, q2~21 ! – maximum allowed deviation from the arc: dR~21 if getValues then R~21 = max(tol, get(1)) q1~21 = get(1): q2~21 = get(1) dR~21 = max(tol, get(1)) getValues = 0 endif !Return: ! – number of approximating line segments: nSegments~21 ! – end points of the line segments – ON THE STACK !
(note there will be nSegments~21 + 1 end points)
!Calculation: !Number of line segments to use nSegmentsMin~21 = int(1 + abs(q2~21 -q1~21)/45) dq~21 = acs((R~21 - dR~21)/R~21) nSegments~21 = max(nSegmentsMin~21, int(180/dq~21)) !Angle increment dq~21 = (q2~21 - q1~21)/nSegments~21 !Put all end points on the stack for qi~21 = q1~21 to q2~21 + dq~21/2 step dq~21 put R~21*cos(qi~21), R~21*sin(qi~21) next qi~21 return
Click on the 2D Full View button to run the script (figure 207). Try setting different values for all the parameters.
Figure 207 – An arc approximated using a series of short line segments. The more line segments used, the smoother the curve.
Chapter 16: Approximating Curves - Ellipses 223
16.2 Ellipses An ellipse is a circle that has been scaled differently in the x and y directions (figure 208). You could think of it as a circle that has been squashed or stretched along one of the axes. Whereas a point on the circumference of a circle is
( R cos θ , R sin θ ) , the corresponding point on an ellipse is ( A cos θ , B sin θ ) . Apart from that minor difference, our sub-routine to run a set of line segments around the circumference of an ellipse is similar to the one we used for a circle. When we calculate the angle step size, we’ll use the smaller of the two radii. A re-usable sub-routine is given below. "22 Approximate Elliptical Arc": !Input: ! – radii: R1~22, R2~22 ! – start and end angles: q1~22, q2~22 ! – maximum allowed deviation from the arc: dR~22 if getValues then
Figure 208 – An ellipse is simply a circle that has been stretched or squashed along orthogonal axes.
R1~22 = max(tol, get(1)) R2~22 = max(tol, get(1)) q1~22 = get(1) q2~22 = get(1) dR~22 = max(tol, get(1)) getValues = 0 endif !Return: ! – number of approximating line segments: nSegments~21 ! – end points of the line segments – ON THE STACK !
(note there will be nSegments~21 + 1 end points)
!Calculation: !Number of line segments to use nSegmentsMin~22 = int(1 + abs(q2~22 -q1~22)/45) R~22 = min(R1~22, R2~22) dq~22 = acs((R~22 - dR~22)/R~22) nSegments~22 = max(nSegmentsMin~22, int(180/dq~22)) ! Angle increment dq~22 = (q2~22 - q1~22)/nSegments~22
224 Chapter 16: Approximating Curves - Helices !Put all end points on the stack for qi~22 = q1~22 to q2~22 + dq~22/2 step dq~22 put R1~22*cos(qi~22), R2~22*sin(qi~22) next qi~22 return
16.3 Helices A helix is a coil that wraps itself around a cylinder. It can be used to model a spring, the thread of a bolt, or a worm gear. Imagine putting the tip of your finger on one end of the helix, and tracing its path to the other end. When viewed along the axis, your finger will sweep through a full circle for each of its coils. When viewed from the side, your finger tip goes up and down, but runs along the axis at a steady rate. We can describe this motion parametrically using an angle θ . x = R cos(θ ) y = R sin(θ ) = θ z k=
p ( θ / 360 )
where p is the separation between the coils, or the pitch of the helix and R is the radius of the coils. As we did with the circular arcs, we’ll choose an incremental angle that produces a visually smooth curve using as few edges as possible. Example: Helical Spring For this example, we will use the tube statement to create a helical spring (figure 209). The spring will have parameters to control the number of coils, the spring length and the coil radius. The length will be controlled by the parameter zzyzx. The first thing we need to calculate is the distance D between the coils. This is simply the total spring length zzyzx divided by the number of coils n.
D = zzyzx / n Figure 209 – The curved surface
Next, we can calculate the total length of wire used to make the spring. Each coil of the of this helical spring is modeled by spring turns through a full circle of radius R, and the circumference of the circle measures approximating the curve s with 2πR units. At the same time, the coil extends along the axis by a distance of D. The length straight edges and flat planes. of wire required for each coil is thus = L
( 2π R )
2
+D
2
Chapter 16: Approximating Curves - Bezier Splines 225 The total length of wire required for the whole spring is nL. Create a new object with parameters for coil radius (coilRadius), wire radius(wireRadius) and the number of coils (n), and copy the following GDL script into the 3D Script editing window. !Hotspots to control the spring length hotspot 0, 0, 0 hotspot 0, 0, zzyzx !Set the resolution using toler toler .001 !Calculated Constants !Pitch of the helix pitch = zzyzx/n !Total angle totalAngle = 360*n !Incremental angle nSegmentsMin = int(1 + totalAngle/45) dq = acs((coilRadius - 0.002)/coilRadius) dq = max(dq, 5) nSegments = max(nSegmentsMin, n*int(360/dq)) dq = totalAngle/nSegments !Trace the locus of the helix for qi = -dq to totalAngle + dq*1.5 step dq put coilRadius*cos(qi), coilRadius*sin(qi), zzyzx*(qi/totalAngle), 0 next qi !Draw the helical spring using a tube element tube 2, nsp/4, 51, 0, 0, 901, wireRadius, 360, 4001, get(nsp)
Click the 3D View button to run the script. You can use the parameters to adjust the number of coils, the spring height and radii of wire and coil. If you place an instance of the object into the project, you can use the 3D hotspots to extend or contract the spring.
226 Chapter 16: Approximating Curves - Bezier Splines
16.4 Bezier Splines Splines are mathematical curves that can be fitted to data. Traditionally, a spline consists of a set of interdependent functions that must be solved simultaneously. Each function starts at one data point, and ends at the next. Pairs of adjacent functions share the same slope and second derivative where they meet. As we’ll see in the next section, there is only one solution to the resulting system of equations, so traditional splines choose their own course through the data points, making them difficult to control. Bezier splines differ from traditional splines in that the individual functions can be adjusted independently. This makes them very easy to control, and ideal for certain types of freeform modeling. Whichever type of spline you choose, an independent spline equation can be generated for each co-ordinate. Equations are constructed that make x and y functions of an arbitrary variable t, rather than making y a function of x. In 3-space this approach allows us to create curves that form complete loops, or even knots. In this section we will take a look at Bezier splines.
16.4.1
Quadratic Bezier Splines
The simplest of the Bezier splines is quadratic. To construct a quadratic Bezier spline, you need two end points
( x , y ) and ( x , y ) , and a control point ( x , y ) . The control point defines the curve’s tangents at the end points. 1
3
1
3
2
2
The easiest way to picture how the curve is drawn is to construct control lines between the end points and the control point. Now imagine a third line segment that stretches between our control lines. The two end points ( x A ( t ) , y A ( t ) )
( x ( t ) , y ( t ) ) of the new line segment are free to slide along ( x , y ) − ( x , y ) respectively. At time t = 0 , the start point of the slider lies on ( x , y ) and the end point is located at ( x , y ) . As time and 2
B
B
2
3
the control lines
(x , y )−(x , y ) 1
1
2
2
and
3
1
1
2
2
progresses, the points slide with constant speed along the line segments
until, at t = 1 , the start point reaches ( x2 , y2 ) and the end point reaches
(x , y ) 3
3
(figure 210).
Finally, imagine a point ( x ( t ) , y ( t ) ) that runs along our moving line segment ( x ( t ) , y ( t ) ) − ( x ( t ) , y ( t ) ) . At time t = 0 , the point lies A
A
B
B
on the start point of the moving line segment, and at time t = 1 it Figure 210 – A quadratic Bezier spline is reaches the end of the line segment.
constructed from three vertices that control the The resulting locus of the sliding point ( x ( t ) , y ( t ) ) traces out the spline’s end points and tangents..
Chapter 16: Approximating Curves - Bezier Splines 227 quadratic Bezier spline. Now we have set up a model for a quadratic Bezier spline, we go ahead and express it as an equation in t . It turns out that we can do this quite painlessly. Let’s consider the start point
(x
A
, y A ) of our moving line. We can write an expression for each co-ordinate by
considering the end conditions. We know that x A (0) = x1 , and x A (1) = x2 . We also know that the expression is linear, since the point moves at a constant rate between x1 and x2 . Thus we can write:
x A = x1 (1 − t ) + x2 t Similarly, we can write expressions for the co-ordinates of points ( x ( t ) , y ( t ) ) and ( x ( t ) , y ( t ) ) : A
A
B
B
y (1 − t ) + y t
x A = x1 (1 − t ) + x2 t y A=
1
2
(1 − t ) + y t
x B= x2 (1 − t ) + x3 t y B= y 2
3
Now let’s consider the sliding point ( x ( t ) , y ( t ) ) . Using the same argument, we can write an expression for x (t ) :
x= x A (1 − t ) + xB t Substituting the expression we obtained for x A (t ) and xB (t ) , we get:
x = x1 (1 − t ) + 2 x2 (1 − t )t + x3t 2
2
Similarly, for y (t ) we obtain the expression:
y = y1 (1 − t ) + 2 y2 (1 − t )t + y3t 2
2
Note that these formulae have been derived for a 2-dimensional spline. However, they also apply to the case of a 3dimensional spline, where z ( t ) also varies between the start and end points. If we introduce a variable s such that s = 1 − t , we can re-write our equations for x, y and z as follows.
x =x1 s + 2 x2 st + x3t 2
2
y =y1 s + 2 y2 st + y3t 2
2
228 Chapter 16: Approximating Curves - Bezier Splines
z =z1 s + 2 z 2 st + z3t 2
2
A GDL sub-routine to calculate and draw the points along a quadratic Bezier spline in the 2D symbol might read as follows. Note that the sub-routine places the calculated vertices on the stack for use by the main script. I’ve included the call to the subroutine to give the idea of a possible context, the values could equally well have been stored in the values of an array variable. !Use straight lines to approximate a quadratic Bezier spline getValues = 1 put x1, y1, x2, y2, x3, y3, .1 gosub “8 Quadratic Bezier Spline 2D” poly2 nsp/2, 1, get(nsp) end “8 Quadratic Bezier Spline 2D”: !Input: !
- start point (x1~8, y1~8)
!
- control point (x2~8, y2~8)
!
- end point (x3~8, y3~8)
!
- step size dt~8 (0 < t < 1) if getValues then x1~8 = get(1): y1~8 = get(1) x2~8 = get(1): y2~8 = get(1) x3~8 = get(1): y3~8 = get(1) dt~8 = get(1) getValues = 0 endif
!Return: !
- number of vertices = nsp/2
!
- vertex co-ordinates (x1, y1, x2, y2, x3, y3, ...) ON THE STACK
!Calculation: !Interpolate the quadratic Bezier spline. for t~8 = 0 to 1 + dt~8/2 step dt~8
Chapter 16: Approximating Curves - Bezier Splines 229 s~8 = 1 – t~8 x~8 = x1~8*s~8^2 + 2*x2~8*s~8*t~8 + x3~8*t~8^2 y~8 = y1~8*s~8^2 + 2*y2~8*s~8*t~8 + y3~8*t~8^2 put x~8, y~8 next t~8 return
This works fine for a 2D spline, and it would be easy enough to create a variation for a spline that bends in all 3 directions. However we can do better than that. Rather than constructing a sub-routine specifically designed for 2 or 3 dimensions, we’ll take advantage that the spline equations for the 3 dimensions are independent. Each equation is a function only of t. Thus the spline equation for x(t) does not involve the variables y or z. Because the equations are independent, we can solve for each equation separately. If we construct a generalized sub-routine that returns a set of values for one co-ordinate, we can use the same sub-routine for any co-ordinate. “23 Quadratic Bezier Spline x(t)”: !Input: !
- a start point x1~23
!
- a control point x2~23
!
- an end point x3~23
!(This sub-routine works for any variable(x(t), y(t), z(t) etc.) !the input variable name is representative only). if getValues then x1~23 = get(1): x2~23 = get(1): x3~23 = get(1) nt~23 = get(1) getValues = 0 endif !Return: !
- the quadratic Bezier spline approximated by nt_23 line segments.
!
- a set of line segment end points x(t) ON THE STACK
!Calculation: for i~23 = 0 to nt~23 t~23 = i~23/nt~23 s~23 = 1 – t~23 put x1~23*s~23^2 + 2*x2~23*s~23*t~23 + 3*x3~23*t~23^2 next i~23 return
230 Chapter 16: Approximating Curves - Bezier Splines
16.4.2
Quadratic Bezier Spline through 3 Points
To trace a quadratic Bezier spline starting at (x1, y1), passing through (x, y), and ending at (x3, y3), we need to construct a control point (figure 211). The position of the control point (x2, y2), it turns out, can be quite easily calculated. Take the mid-point of the line segment joining the start and end points, and draw a line from this to the mid point of the spline (x, y). Double the length of this line segment to find the control point (x2, y2). With these adjustments to the regular quadratic Bezier spline, our modified equations can be written as follows. Figure 211 – How to construct a quadratic Bezir
x1 + x3
y1 + y3
x2 =x + x − y2 =y + y −
z 2 =z + z −
=2 x −
2
2 z1 + z3 2
x1 + x3
spline through three points.
2
=2 y − y1 + y3 2
=2 z − z1 + z3 2
A GDL sub-routine to trace a quadratic spline through three points is given below. “9 Quadratic Bezier Spline through Three Points 2D”: !Input: ! - start point (x1~9, y1~9) ! - mid-point (x2~9, y2~9) ! - end point (x3~9, y3~9) ! - step size dt~9 (0 < t < 1) if getValues then x1~9 = get(1): y1~9 = get(1) x2~9 = get(1): y2~9 = get(1) x3~9 = get(1): y3~9 = get(1) dt~9 = get(1) getValues = 0 endif !Return: !
- number of vertices = nsp/2
Chapter 16: Approximating Curves - Bezier Splines 231 !
- vertex co-ordinates (x1, y1, x2, y2, x3, y3, ...) ON THE STACK
!Calculation: !Adjust the Midpoint x2~9 = 2*x2~9 – (x1~9 + x3~9)/2 y2~9 = 2*y2~9 – (y1~9 + y3~9)/2 !Interpolate the quadratic Bezier spline. for t~9 = 0 to 1 + dt~9/2 step dt~9 s~9 = 1 – t~9 x~9 = x1~9*s~9^2 + 2*x2~9*s~9*t~9 + x3~9*t~9^2 y~9 = y1~9*s~9^2 + 2*y2~9*s~9*t~9 + y3~9*t~9^2 put x~9, y~9 next t~9 return
As in the previous example, a generalized variation of this sub-routine can be made to make it useful for both 2D and 3D splines. “14 Quadratic Bezier Spline through Three Points x(t)”: !Input: !
- a start point x1~14
!
- a mid point x2~14
!
- an end point x3~14
!(This sub-routine works for any variable(x(t), y(t), z(t) etc.) !the input variable name is representative only). if getValues then x1~14 = get(1): x2~14 = get(1): x3~14 = get(1) nt~14 = get(1) getValues = 0 endif !Return: !
- the quadratic Bezier spline approximated by nt_14 line segments.
!
- a set of line segment end points x(t) ON THE STACK
!Calculation: !Adjust the Midpoint x2~14 = 2*x2~14 – (x1~14 + x3~14)/2 !Interpolate the quadratic Bezier spline. for i~14 = 0 to n~14 t~14 = i~14/n~14
232 Chapter 16: Approximating Curves - Bezier Splines s~14 = 1 – t~14 put x1~14*s~14^2 + 2*x2~14*s~14*t~14 + x3~14*t~14^2 next i~14 return
16.4.3
Cubic Bezier Splines
The cubic Bezier spline is perhaps the most commonly used of all the splines. ArchiCAD uses cubic Bezier splines for the 2D Spline tool. Why are cubic Bezier splines so popular? Basically, they provide an extra level of freedom. You can set the tangent at each end of the spline independently, which is not possible for quadratic splines. This means, for instance, that you can use a set of cubic Bezier splines to form a closed loop. To construct a cubic Bezier spline, you need two end points Figure 212 – A cubic Bezier spline is constructed ( x1 , y1 ) and ( x4 , y4 ) , and two control points ( x2 , y2 ) and ( x3 , y3 ) . from 4 vertices that control the spline’s end points and tangents.
The control points define the curve’s tangent at the end points.
The easiest way to picture how the curve is drawn is to imagine three new points A = ( x , y ) , B = ( x , y ) and A
C = (x , y C
C
A
B
B
) that slide along the line segments ( x , y ) − ( x , y ) , ( x , y ) − ( x , y ) and ( x , y ) − ( x , y ) respectively. 1
1
2
2
2
2
3
3
3
3
4
4
( x , y ) , ( x , y ) and ( x , y ) . As time progresses, the points slide at constant speed along the line segments until, at t = 1 , they reach the end points ( x , y ) , ( x , y ) and ( x , y ) (figure
At time t = 0 , the points A , B and C are at
1
1
2
2
3
3
2
2
3
3
4
4
212). Construct sliding line segments AB and BC . Imagine two new points D and E that slide along these new line segments. As before, at time t = 0 , D is sitting on point A , and E is located at point B . The points slide at constant speed until at t = 1 they reach points B and C respectively. Finally, construct a line segment DE . Imagine a new point ( x, y ) that slides along line segment DE , starting at D when t = 0 , and arriving safely at E at time t = 1 . The resulting locus of point ( x, y ) traces out the cubic Bezier spline. We want to express our cubic Bezier spline as an equation in t as we did with the quadratic Bezier spline. Let’s first consider point A . We can write an expression for x A (t ) by considering the end conditions. We know that x A (0) = x1 ,
Chapter 16: Approximating Curves - Bezier Splines 233 and x A (1) = x2 . We also know that the expression is linear, since the point moves at a constant rate between
x1 and x2 .
As there is only one such function, then any expression that satisfies these requirements is a valid solution. Thus we can write:
x A = x1 (1 − t ) + x2 t As before, we will use the substitution s = 1 − t so that our equation becomes
x= x1 s + x2 t A Similarly, we can write expressions for the co-ordinates of points B and C :
yA = y1 s + y2 t
x= x1 s + x2 t A
xB = x2 s + x3t
yB = y 2 s + y3 t
xC = x3 s + x4 t
yC = y3 s + y 4 t
Now let’s consider the point D . Using the same argument, we can write an expression for xD (t ) :
xD (= t ) x A s + xB t Substituting the expression we obtained for x A (t ) , xB (t ) and xC (t ) , we get:
xD =x1 s + 2 x2 st + x3t 2
2
Similarly, for the points A′′ and B ′′ we obtain the expressions:
2 2 y D =y1 s + 2 y2 st + y3t 2 2 xE =x2 s + 2 x3 st + x4 t 2 2 y E = y2 s + 2 y3 st + y4 t xD =x1 s + 2 x2 st + x3t 2
2
To find point P we apply the same logic to obtain an expression for x (t ) and y (t ) :
x =x1 s + 3 x2 s t + 3 x3 st + x4 t 3
2
2
3
y =y1 s + 3 y2 s t + 3 y3 st + y4 t 3
2
2
3
Note that these formulae have been derived for a 2-dimensional spline. However, they also apply to the case of a 3dimensional spline, where z ( t ) also varies between the start and end points.
234 Chapter 16: Approximating Curves - Bezier Splines A GDL sub-routine to calculate and draw the points along a cubic Bezier spline in the 2D symbol might read as follows: “10 Cubic Bezier Spline 2D”: !Input: !
- start point (x1~10, y1~10)
!
- start control point (x2~10, y2~10)
!
- end control point (x3~10, y3~10)
!
- end point (x4~10, y4~10)
!
- step size dt~10 (0 < t < 1) if getValues then x1~10 = get(1): y1~10 = get(1) x2~10 = get(1): y2~10 = get(1) x3~10 = get(1): y3~10 = get(1) x4~10 = get(1): y4~10 = get(1) dt~10 = get(1) getValues = 0 endif
!Return: !
- number of vertices = nsp/2
!
- vertex co-ordinates (x1, y1, x2, y2, x3, y3, ...) ON THE STACK
!Calculation: !Interpolate the cubic Bezier spline. for t~10 = 0 to 1 + dt~10/2 step dt~10 s~10 = 1 – t~10 x~10 = x1~10*s~10^3 + 3*x2~10*s~10^2*t~10 + 3*x3~10*s~10*t~10^2 + x4~10*t~10^3 y~10 = y1~10*s~10^3 + 3*y2~10*s~10^2*t~10 + 3*y3~10*s~10*t~10^2 + y4~10*t~10^3 put x~10, y~10 next t~10 return
Our generalized variation for calculating the values of just one component of the spline would read as follows. “13 Cubic Bezier Spline x(t)”: !Input: !
- a start point x1~13
!
- a start control point x2~13
!
- an end control point x3~13
!
- an end point x4~13
!(This sub-routine works for any variable(x(t), y(t), z(t) etc.)
Chapter 16: Approximating Curves - Bezier Splines 235 !the input variable name is representative only). if getValues then x1~13 = get(1): x2~13 = get(1) : x3~13 = get(1): x4~13 = get(1) nt~13 = get(1) getValues = 0 endif !Return: !
- the cubic Bezier spline approximated by nt_23 line segments.
!
- a set of line segment end points x(t) ON THE STACK
!Calculation: for i~13 = 0 to n~13 t~13 = i~13/n~13 s~13 = 1 – t~13 put x1~13*s~13^3 + 3*x2~13*s~13^2*t~13 + 3*x3~13*s~13*t~13^2 + x4~13*t~13^3 next i~13 return
The simplest way to approximate Bezier splines as line segments, is to use a constant step size for t as in the above script. While this is not always ideal, it is very simple to implement and robust. Try it Yourself: Create a Cubic Bezier Spline To see some cubic Bezier splines in action, create a new object with a length_type array parameter splineVertex[][] that has 4 rows (one for each control point) and 3 columns (for the x, y, z values). Type some default values into the cells, taking care that there are no two rows having identical values (figure 213). In the 2D Script, we’ll provide dynamic hotspots to move the 4 vertices. We’ll use project2 to create a projection of the 3D spline, but to prove there are no smoke-and-mirror tricks going on, we’ll also trace the centerline of the spline Figure 213 – Set some (x, y, z) co-ordusing a 2D approximation. Copy the 13 Cubic Bezier Spline x(t) sub-routine into the Master Script editing window, between two lines goto “Continue” and “Continue”: as shown below. goto "Continue" !Common Sub-routines "13 Cubic Bezier Spline x(t)":
inates for the spline control points.
236 Chapter 16: Approximating Curves - Bezier Splines return "Continue":
Because we’ve added the sub-routine to the Master Script, it will be available to both the 2D Script and the 3D Script. Now copy the following into the 2D Script editing window. !Hotspots for the spline vertices for i = 1 to 4 add2 splineVertex[i][1], splineVertex[i][2] !Y - control hotspot2 0, -splineVertex[i][2], iHotspot + 1, splineVertex[i][2], 1 + 128 hotspot2 0, 0, iHotspot + 2, splineVertex[i][2], 2 hotspot2 0, -splineVertex[i][2] - 1, iHotspot + 3, splineVertex[i][2], 3 iHotspot = iHotspot + 3 !X - control hotspot2 -splineVertex[i][1], 0, iHotspot + 1, splineVertex[i][1], 1 + 128 hotspot2 0, 0, iHotspot + 2, splineVertex[i][1], 2 hotspot2 -splineVertex[i][1] - 1, 0, iHotspot + 3, splineVertex[i][1], 3 iHotspot = iHotspot + 3 del 1 next i !Lines to show the control points - only show up when moving the hotpsots if glob_context > 20 then line2 splineVertex[1][1], splineVertex[1][2], splineVertex[2][1], splineVertex[2][2] line2 splineVertex[3][1], splineVertex[3][2], splineVertex[4][1], splineVertex[4][2] endif !Create a simple projection of the 3D spline project2 3, 270,
2
Chapter 16: Approximating Curves - Bezier Splines 237 !Draw the corresponding 2D spline to show there is no trickery and to provide hotlines !Construct an array containing the co-ordinates of the line segment end points dim xy[][] for j = 1 to 2 !Call the 13 Cubic Bezier Spline x(t) macro to put a set of vertices on the stack getValues = 1 for i = 1 to 4 put splineVertex[i][j] next i put 36
!We'll use 36 line segments to approximate the curve
gosub "13 Cubic Bezier Spline x(t)" !Collect the points from the stack nVert = nsp for i = 1 to nVert xy[i][j] = get(1) next i next j !Draw the line segments for i = 1 to nVert - 1 line2 xy[i][1], xy[i][2], xy[i + 1][1], xy[i + 1][2]
Figure 214 – Place an instance of the object onto a plan view, and try adjusting the dynamic hotspots to change the control points graphically.
hotline2 xy[i][1], xy[i][2], xy[i + 1][1], xy[i + 1][2] next i end
Even though we haven’t yet given out object a 3D Script, we can start to get a feel for how it will work. Save the object, and place an instance of it into the ArchiCAD plan view (figure 214). Try adjusting the hotspots, and you will see how the spline always remains tangential to the control lines that appear as you move the dynamic hotspots (figure 215). Our next step is to create a 3D representation of the spline. We’ll model the spline using a tube with round cross-section, so add a new length-type parameter tubeRadius to the list, and give it a default value of say half an inch. We’ll also add a material-type parameter Figure 215 – By moving the tubeMat and set it to Stainless Steel. Because my default z-values are all zero, my spline control points, the spline takes on will initially lie in the x-y plane. However, we’ll add some 3D dynamic hotspots to pull different shapes. the vertices up and down to add some interest to the curve. Copy the following script into the object’s 3D Script editing window.
238 Chapter 16: Approximating Curves - Bezier Splines !Hotspots for the control points for i = 1 to 4 add splineVertex[i][1], splineVertex[i][2], splineVertex[i][3] !Z - control hotspot 0, 0, -splineVertex[i][3], iHotspot + 1, splineVertex[i][3], 1 + 128 hotspot 0, 0, 0, iHotspot + 2, splineVertex[i][3], 2 hotspot 0, 0, -splineVertex[i][3] - 1, iHotspot + 3, splineVertex[i][3], 3 iHotspot = iHotspot + 3 del 1 next i !Lines to show the control points - only show up when moving the hotpsots if glob_context > 20 then lin_ splineVertex[1][1], splineVertex[1][2], splineVertex[1][3], splineVertex[2][1], splineVertex[2][2], splineVertex[2][3] lin_ splineVertex[3][1], splineVertex[3][2], splineVertex[3][3], splineVertex[4][1], splineVertex[4][2], splineVertex[4][3] endif !Draw the 3D spline !Construct an array containing the co-ordinates of the line segment end points dim xyz[][] for j = 1 to 3 !Call the 13 Cubic Bezier Spline x(t) macro !to put a set of vertices on the stack getValues = 1 for i = 1 to 4 put splineVertex[i][j] next i put 36
!We'll use 36 line segments to approximate the curve
gosub "13 Cubic Bezier Spline x(t)" !Collect the points from the stack nVert = nsp for i = 1 to nVert xyz[i][j] = get(1)
Chapter 16: Approximating Curves - Bezier Splines 239 next i next j !Put the vertices on the stack for use by the tube command !Lead-in put 2*xyz[1][1] - xyz[2][1], 2*xyz[1][2] - xyz[2][2], 2*xyz[1][3] - xyz[2][3], 0 !Main points on the spline
Figure 216 – The projection of the 3D tube overlays the original 2D symbol.
for i = 1 to nVert put xyz[i][1], xyz[i][2], xyz[i][3], 0 next i !Tail-out put 2*xyz[nVert][1] - xyz [nVert - 1][1], 2*xyz[nVert][2] - xyz[nVert - 1][2], 2*xyz[nVert][3] - xyz[nVert - 1][3], 0 !Draw the tube material tubeMat tube 2, nsp/4, 51, 0, 0, 900, tubeRadius, 360, 4001,
Figure 217 – In 3D a tube runs along the splinar path.
get(nsp) end
Save the object, and go back to the floor plan window. The spline you placed earlier now has a thickness where the 3D projection overlays the 2D line view (figure 216). Notice how, in this example, the structure of the 2D Script and 3D Script are nearly identical. This often happens in scripts where the 2D symbol is designed to closely match the 3D model. In such cases you can copy the 3D Script into the 2D Script, and work through each line, substituting 2D commands for the 3D ones. Now select the spline and generate a 3D view of the selection (figure 217). Select the 3D spline, and try moving the hotspots up and down (figure 218).
Figure 218 – Adjust the control points using dynamic hotspots in 3D.
240 Chapter 16: Approximating Curves - Traditional Cubic Splines
16.5 Traditional Cubic Splines Bezier splines are great if you want a high degree of control over a curve. However, they have the drawback that they require 1 or 2 control points for each piece of the spline. The programmer (or the designer using the library part) has to define precisely how the curve behaves at each data point. This can be tedious when you simply want to run a curve through some data. So long as you’re not too fussy about the exact path the curve takes, it can be convenient to simply provide the data points and leave the curve tracing to an algorithm. The regular cubic spline family of curves provides such interpolating curves Feed in any set of data points, and a smooth curve is automatically calculated that passes smoothly through the points (figure 219). Just be aware that moving a single data point will change the shape of the entire curve Figure 219 – A traditional cubic – the effect is not localized as in the case of the Bezier splines. This tends to render spline finds its own path through a set traditional cubic splines rather wild and unmanageable. Use them with caution if of points. you’re trying to reproduce a well-defined shape.
16.5.1
Piecewise Definition
A cubic spline is not a single mathematical function. Rather, a separate function is interpolated between each pair of data points. The spline is the complete set of all these functions. As the name suggests, the interpolated functions of the spline are cubic polynomials of the form
fi ( x ) =ai + bi x + ci x + d i x . Each function starts at data point ( xi , yi ) and ends at ( xi +1 , yi +1 ) .If there are n + 1 data 2
points, we must find n interpolating functions. The key to finding these functions is to calculate the coefficients ai , bi ,
ci and d i . We can simplify the problem by defining each function as if it passes through the origin. To obtain the actual function values, we will add xi to the x-value and yi to the y-value after we have calculated the simplified function. Our simplified functions, then, have the form fi ( x ) =bi x + ci x + d i x . As we will see later, we will need to calculate first and second 2
3
derivatives of these functions fi ′( x ) = bi + 2ci x + 3d i x and fi′′ ( x= ) 2ci + 6d i x . 2
16.5.2
A System of Linear Equations
Each function is defined on the interval 0 ≤ x ≤ ( xi +1 − xi ) . For convenience, we will use ∆xi to mean ( xi +1 − xi ) , and
Chapter 16: Approximating Curves - Traditional Cubic Splines 241
∆yi to mean
(y
i +1
− yi ) . At each data point (except for the first and last), one function ends and another starts. We’ll
call the function ending at the data point the left function, and the function starting at the data point the right function. At these ‘middle’ data points, then, the values of the left and right functions must match. To get a smooth, continuous curve, the slopes of the functions must match at each data point. We will also insist that the second derivatives of the left and right functions match at the data points. At each data point then, the following conditions must be met.
1.
For each of the n cubic functions, the function value at ∆xi must equal ∆yi .
fi ( ∆xi ) = ∆yi
(
bi ( ∆xi ) + ci ∆xi 2.
2
) + d ( ∆x ) =∆y i
i
For the first n – 1 functions, the slope (first derivative) of the left and right functions must match.
fi ′( ∆= xi )
(
)
fi +′1 ( 0 ) ⇒ bi + 2ci ( ∆xi ) + 3d i ∆x= bi +1 i
(
bi + ci ( 2 ∆xi ) + d i 3∆xi 3.
(1)
3
i
2
)−b
i +1
= 0
2
(2)
For the first n – 1 functions, the second derivatives must match. If the left function has reached a maximum at the boundary, then the right function must start at a maximum.
fi ′′( ∆= xi )
fi +′′1 ( 0 ) ⇒ 2ci + 6 d i ( ∆= xi ) 2ci +1
ci + d i ( 3∆xi ) − ci +1 = 0
(3)
For each function (except the last one) we now have three equations in the coefficients bi , ci and d i . The values of ∆xi and ∆yi are constants of our equations, determined by the given data points. This gives us 3n – 2 linear equations in 3n variables.
242 Chapter 16: Approximating Curves - Traditional Cubic Splines We can write these equations in matrix form.
∆x1 1
∆x1
∆x1
2
3
2 ∆x1
3∆x1
2
−1
1
3∆x1
−1 ∆x2
∆x2
∆x2
1
2 ∆x2
3∆x2
1
3∆x2
2
3
−1
2
−1 ∆x3
∆x3
∆x3
1
2 ∆x3
3∆x3
1
3∆x3
2
3
2
b1 ∆y1 c 0 1 d1 0 b2 ∆y2 c2 0 = d2 0 b ∆y 3 3 −1 c3 0 d 0 −1 3 etc. etc. etc.
The final row of this system is:
(
∆xn
...
∆xn
∆xn
2
3
( ∆y ) ) (b ) = n
n
If you look at the matrix, you’ll see that it is nearly triangular, in row-reduced echelon form. If it was triangular, the solution would be easy to obtain by back-substitution. It is easy enough to re-write the matrix in a diagonal form. All we need do is re-write our equations to remove the first terms from each instance of equations 2 and 3. To remove the first term for each instance of equation (2), we will subtract a multiple of equation (1).
(
bi ( ∆xi ) + ci ∆xi
(
2
bi ( ∆xi ) + ci 2 ∆xi
(
ci ∆xi
2
) + d ( ∆x ) =∆y 2
i
i
0 ) + d ( 3∆x ) − b ( ∆x ) = 3
i
i +1
i
) + d ( 2∆x ) − b ( ∆x ) = 3
i
(1)
3
i
i +1
i
i
i
−∆yi
(2)
×∆xi → (2*)
(2*) – (1)
→ (2**)
To remove the first term for each instance of equation (3), we will subtract a multiple of equation (2**).
ci + d i ( 3∆xi ) − ci +1 = 0
(
d i ∆xi
3
(3) ×∆xi
) + b ( ∆x ) − c ( ∆x ) =∆y 2
i +1
i
i +1
i
i
2
→ (3*)
(3*)–(2**) → (3**)
Chapter 16: Approximating Curves - Traditional Cubic Splines 243 Now we can re-write our system of equations as follows.
∆x1
∆x1
2
∆x1
∆x1
2
2 ∆x1 ∆x1
3
3
3
−∆x1 ∆x1
−∆x1
∆x2
∆x2
2
∆x2
∆x2
2
2 ∆x2
2
∆x2
3
3
3
−∆x2 ∆x2
−∆x2
∆x3
∆x3
2
∆x3
∆x3
2
2 ∆x3
2
3
3
b1 ∆y1 c −∆y 1 1 d1 ∆y1 b2 ∆y2 c2 −∆y2 = d 2 ∆y2 b ∆y 3 3 −∆x3 c3 −∆y3 d ∆y 3 3 etc. etc. etc.
To solve the system, we need two more equations, which we can obtain by considering the end conditions. The final two equations are provided by our choice of end conditions. Depending on how we choose the end conditions, we can produce a different ‘flavour’ of spline.
16.5.3
Closed Spline
One solution to this problem is to imagine that the first function is repeated at the end. As we’ll see shortly, this approach will provide a spline that can be used to form a continuous, closed curve. In other words the slope (1st derivative) and 2nd derivative at the end of the final function must match that at the start of the first function. For a closed cubic spline, then, the final boundary conditions can be written:
(
bn + 2cn ( ∆xn ) + 3d n ∆xn
2
)−b
1
= 0
(1st derivatives match)
cn ( ∆xn ) + 3d n ( ∆xn ) − c1 = 0
(2nd derivatives match)
In matrix form the last 2 rows of the system look like this.
−∆xn ∆x n
∆xn −∆xn
2
2
2 ∆xn cn = 3 ∆xn d n 3
−∆y n ∆y n
244 Chapter 16: Approximating Curves - Traditional Cubic Splines
16.5.4
Natural Spline
A natural spline is an open spline that starts and ends with zero second derivative. In practice this means that the spline starts and ends at a point of inflexion, and is instantaneously straight. As the name suggests, this provides a curve that appears natural to the eye – a sort of ‘spline of best fit’. Our final two equations will set the condition that the second derivatives at the two ends to zero. In other words, the spline straightens out at the ends.
cn + d n ( 3∆xn ) = 0 (the 2nd derivative of the last function is zero at the end point) c1 = 0
(the 2nd derivative of the first function is zero at the start point)
For a natural spline, our final two equations can be written as:
c = 0 1 3∆xn n d 0 n
1
16.5.5
Clamped Spline
The term clamped is given to a spline whose initial and final directions are fixed. In GDL we might use dynamic hotspots to define these directions. The final two equations of our system will be:
(
bn + cn ( 2 ∆xn ) + d n 3∆xn
2
v (the derivative of the last function has a value v at the end point) )=
b1 = u (the derivative of the first function has a value u at the start point) In matrix form, these can be written as follows.
1 16.5.6
1
2 ∆xn
3∆xn cn = v d u n 2
Create a 3D Cubic Spline
The trick to creating a 3D (or a 2D) spline is to create a set of 3 (or 2) independent splines in each of the three variables x, y and z. Each spline is a function of a parameter t. If you like you can think of t as a time, in which case the spline
Chapter 16: Approximating Curves - Traditional Cubic Splines 245 represents motion through space. We will assume that the interval ∆t is the same for each function, and we will set that interval to unity for simplicity. To see how this works, we’ll create an object that can interpolate a closed, clamped or natural cubic spline through any given data points (figure 220). Within the GDL script, we will include a re-usable sub-routine that can be used to generate the co-efficients for either one of our three splines. Create a new object with material type parameters nodeMat and pathMat, and a length type array variable xyz[][] with 10 rows and 3 columns. The length variable is a list of the locations of the path nodes, so each row should have three columns for the x, y and z co-ordinates of the node. Add an integer-type parameter n and set its value to 4. Add parameters initialDn[] and finalDn[] to define the directions at the ends for a clamped spline. Copy the following script into the 3D Script editing window. !Array Variables Used in Sub-routines dim dx~12[], A~12[][], b~12[], c~12[] resol 18 !End Directions
Figure 220 – Three flavours of cubic spline – closed, clamped and natural.
if splineType = "Clamped" then !Initial Direction lin_ xyz[1][1], xyz[1][2], xyz[1][3], xyz[1][1] + initialDn[1], xyz[1][2] + initialDn[2], xyz[1][3] + initialDn[3] add xyz[1][1] + initialDn[1], xyz[1][2] + initialDn[2], xyz[1][3] hotspot 0, 0, 0, e + 1, initialDn[3], 1 + 128 hotspot 0, 0, initialDn[3], e + 2, initialDn[3], 2 hotspot 0, 0, -1, e + 3, initialDn[3], 3 e = e + 3 del 1 !Final Direction lin_ xyz[n][1], xyz[n][2], xyz[n][3], xyz[n][1] + finalDn[1], xyz[n][2] + finalDn[2], xyz[n][3] + finalDn[3] add xyz[n][1] + finalDn[1], xyz[n][2] + finalDn[2], xyz[n][3] hotspot 0, 0, 0, e + 1, finalDn[3], 1 + 128
246 Chapter 16: Approximating Curves - Traditional Cubic Splines hotspot 0, 0, finalDn[3], e + 2, finalDn[3], 2 hotspot 0, 0, -1, e + 3, finalDn[3], 3 e = e + 3 del 1 endif !Dynamic Hotspots for the Data Nodes for i = 1 to n add xyz[i][1], xyz[i][2], 0 hotspot 0, 0, 0, e + 1, xyz[i][3], 1 + 128 hotspot 0, 0, xyz[i][3], e + 2, xyz[i][3], 2 hotspot 0, 0, -1, e + 3, xyz[i][3], 3 e = e + 3 del 1 next i !For a Closed spline, add a new node at the end that matches the first node and closes the path if splineType = "Closed" then n = n + 1 for j = 1 to 3 xyz[n][j] = xyz[1][j] next j endif !Decrement n so that it now refers to the number of functions, not the number of data points n = n - 1 !Calculate the coefficients of a Spline given the Data Points xyz[][] dim splineA[][], splineB[][], splineC[][], splineD[][] splineType~12 = splineType n~12 = n for j = 1 to 3 !Initial and final direction components for Clamped splines initialDnComponent~12 = initialDn[j] finalDnComponent~12 = finalDn[j] !Calculate dx, dy and dz for each cubic function for i = 1 to n dx~12[i] = xyz[i + 1][j] - xyz[i][j] next i !Calculate the spline coefficients for each co-ordinate
Chapter 16: Approximating Curves - Traditional Cubic Splines 247 gosub "12 Cubic Spline Coefficients" !Get the returned values for i = 1 to n splineA[i][j] = xyz[i][j] splineB[i][j] = get(1) splineC[i][j] = get(1) splineD[i][j] = get(1) next i next j !Draw the path nodes material nodeMat for i = 1 to n + 1 add xyz[i][1], xyz[i][2], xyz[i][3] sphere .06 del 1 next i !Draw the spline material pathMat dt = .1 for i = 1 to n t0 = 0: t1 = 1 if i = 1 then t0 = -dt if i = n then t1 = 1 + dt/2 + dt for t = t0 to t1 step dt put splineA[i][1] + splineB[i][1]*t + splineC[i][1]*t^2 + splineD[i][1]*t^3, splineA[i][2] + splineB[i][2]*t + splineC[i][2]*t^2 + splineD[i][2]*t^3, splineA[i][3] + splineB[i][3]*t + splineC[i][3]*t^2 + splineD[i][3]*t^3, 0 next t next i tube 2, nsp/4, 3, 0, 0, 901, .04, 360, 4001, get(nsp) end
248 Chapter 16: Approximating Curves - Traditional Cubic Splines "12 Cubic Spline Coefficients": !Input: !
- number of data points (nodes) on the spline: n~12
!
- for clamped splines,
! ! ! !
- the initial direction component: initialDn - the final direction component: finalDn - the distances between each pair of adjacent points measured along the current axis dx~12[i]
!Return: ! !
- for each node of the spline, the co-efficients bi, ci, di of the piecewise equation xi(t) = ai + bi*t + ci*t^2 + di*t^3 ON THE STACK
!Calculation: Set up the matrix A and vector b to solve Ac = b for c !Create a zeroed out square vector 3*n~12 x 3*n~12 m~12 = 3*n~12 for i~12 = 1 to m~12 for j~12 = 1 to m~12 A~12[i~12][j~12] = 0 next j~12 next i~12 !Match the Functions at the Nodes for i~12 = 1 to n~12 - 1 j~12 = 3*(i~12 - 1) !Function Values Match A~12[j~12 + 1][j~12 + 1] = 1 A~12[j~12 + 1][j~12 + 2] = 1 A~12[j~12 + 1][j~12 + 3] = 1 b~12[j~12 + 1] = dx~12[i~12] !First Derivatives Match A~12[j~12 + 2][j~12 + 2] = 1 A~12[j~12 + 2][j~12 + 3] = 2 A~12[j~12 + 2][j~12 + 4] = -1 b~12[j~12 + 2] = -dx~12[i~12] !Second Derivatives Match A~12[j~12 + 3][j~12 + 3] = 1 A~12[j~12 + 3][j~12 + 4] = 1 A~12[j~12 + 3][j~12 + 5] = -1
Chapter 16: Approximating Curves - Traditional Cubic Splines 249 b~12[j~12 + 3] = dx~12[i~12] next i~12 !Function Values Match A~12[m~12 - 2][m~12 - 2] = 1 A~12[m~12 - 2][m~12 - 1] = 1 A~12[m~12 - 2][m~12] = 1 !The Vector b b~12[m~12 - 2] = dx~12[n~12] !Last Two Equations !Closed Spline if splineType~12 = "Closed" then !First Derivatives Match A~12[m~12 - 1][m~12 - 1] = 1 A~12[m~12 - 1][m~12] = 2 A~12[m~12 - 1][1] = -1 b~12[m~12 - 1] = -dx~12[i~12] !Second Derivatives Match A~12[m~12][m~12] = 1 A~12[m~12][1] = 1 A~12[m~12][2] = -1 b~12[m~12] = dx~12[i~12] endif !Clamped Spline if splineType~12 = "Clamped" then !Set Final Derivative A~12[m~12 - 1][m~12 - 2] = 1 A~12[m~12 - 1][m~12 - 1] = 2 A~12[m~12 - 1][m~12] = 3 b~12[m~12 - 1] = finalDnComponent~12 !Set Initial Derivative A~12[m~12][1] = 1 b~12[m~12] = -initialDnComponent~12 endif !Natural Spline if splineType~12 = "Natural" then !Set Final Second Derivative to Zero at the End
250 Chapter 16: Approximating Curves - Traditional Cubic Splines A~12[m~12 - 1][m~12 - 1] = 1 A~12[m~12 - 1][m~12] = 3 b~12[m~12 - 1] = 0 !Set Initial Second Derivative to Zero at the Start A~12[m~12][2] = 1 b~12[m~12] = 0 endif !Zero out the leading non-diagonals on the last two rows for i~12 = m~12 - 1 to m~12 for j~12 = 1 to i~12 - 1 if abs(A~12[i~12][j~12]) > .000001 then scale~12 = A~12[i~12][j~12]/A~12[j~12][j~12] for k = 1 to m~12 A~12[i~12][k] = A~12[i~12][k] - scale~12*A~12[j~12][k] next k b~12[i~12] = b~12[i~12] - scale~12*b~12[j~12] endif next j~12 next i~12 !Calculate the Coefficients c~12[m~12] = b~12[m~12]/A~12[m~12][m~12] c~12[m~12 - 1] = (b~12[m~12 - 1] - A~12[m~12 - 1][m~12]*c~12[m~12])/A~12[m~12-1][m~12-1] for i~12 = m~12 - 2 to 1 step -1 c~12[i~12] = (b~12[i~12] - A~12[i~12][i~12 + 1]*c~12[i~12 + 1] c~12[i~12] = c~12[i~12] - A~12[i~12][i~12 + 2]*c~12[i~12 + 2])/A~12[i~12][i~12] next i~12 !Put the spline coefficients onto the stack for i~12 = 1 to n~12 put c~12[3*(i~12 - 1) + 1], c~12[3*(i~12 - 1) + 2], c~12[3*(i~12 - 1) + 3] next i~12 return
Chapter 16: Approximating Curves - Traditional Cubic Splines 251 Copy the script below into the 2D Script editing window. !Lazy Projection project2 3, 270, 2 !Hotspots to control End Directions: if splineType = "Clamped" then !Initial Direction add2 x[1][1] + initialDn[1], x[1][2] + initialDn[2] hotspot2 0, -initialDn[2], e+1,initialDn[2], 1+128 hotspot2 0, 0, e + 2, initialDn[2], 2 hotspot2 0, -initialDn[2] - 1, e+3,initialDn[2], 3
Figure 221 – If just one node of the spline is adjusted, the entire curve changes. This makes such splines difficult to control.
e = e + 3 hotspot2 -initialDn[1], 0, e + 1, initialDn[1], 1 + 128 hotspot2 0, 0, e + 2, initialDn[1], 2 hotspot2 -initialDn[1] - 1, 0, e + 3, initialDn[1], 3 e = e + 3 del 1 !Final Direction add2 x[n][1] + finalDn[1], x[n][2] + finalDn[2] hotspot2 0, -finalDn[2], e + 1, finalDn[2], 1 + 128 hotspot2 0, 0, e + 2, finalDn[2], 2 hotspot2 0, -finalDn[2] - 1, e + 3, finalDn[2], 3 e = e + 3 hotspot2 -finalDn[1], 0, e + 1, finalDn[1], 1 + 128 hotspot2 0, 0, e + 2, finalDn[1], 2 hotspot2 -finalDn[1] - 1, 0, e + 3, finalDn[1], 3 e = e + 3 del 1 endif !Path Nodes for i = 1 to n add2 x[i][1], x[i][2] hotspot2 0, -x[i][2], e + 1, x[i][2], 1 + 128 hotspot2 0, 0, e + 2, x[i][2], 2 hotspot2 0, -x[i][2] - 1, e + 3, x[i][2], 3 e = e + 3
252 Chapter 16: Approximating Curves - Traditional Cubic Splines hotspot2 -x[i][1], 0, e + 1, x[i][1], 1 + 128 hotspot2 0, 0, e + 2, x[i][1], 2 hotspot2 -x[i][1] - 1, 0, e + 3, x[i][1], 3 e = e + 3 del 1 next i end
Finally, copy the following script into the Parameter Script editing window. values "splineType" "Natural", "Clamped", "Closed"
Save the object, and place an instance into the project plan view. Move the dynamic hotspots in 2D and 3D to make weird, pretzel-like shapes. Compare the different spline types to understand their characteristics. Notice how when you move a single path node, the whole path changes. This makes the regular cubic splines difficult to manage. On the other hand they always provide a wonderfully smooth path so are great if you are modeling inexactly. In this sub-routine, we assumed that the interval ∆t was identical for each of the cubic functions. There is no particular reason why this should be true. By making some minor adjustments to our code we can allow for different values of ∆t for each cubic. The result of increasing ∆t is to ‘loosen’ that segment of the curve, making it ‘loopier’. Reducing the value of ∆t will ‘tighten’ the spline making it ‘straighter’ (figure 222).
Figure 222 – These three splines all share the same set of nodes. By adjusting the value of Дt for any of the intervals, the effect is a quite noticeable change in the resulting shape. Reducing the value results in a ‘straighter’ section of spline.
17 Linear Algebra 101 Every 3D GDL modeling element is defined using a set of vertices, straight edges, and flat polygons. This is true even of curved surfaces – the curved surfaces are approximated by a net of numerous planar facets. The vertices, straight edges and polygons used to define GDL elements are referred to as primitives in GDL. Given that they play such a vital role, I’d like to devote some time to developing resources that deal with calculations related to vertices, lines and planes. Mathematical equations that give rise to straight lines and planes are referred to as linear equations. The branch of mathematics dedicated to the study of linear equations is named linear algebra. As a GDL programmer, linear algebra is one of the most important tools you Figure 223 – The term ‘Linear Algebra’ suggests that this can have in your mathematical toolbox. topic is somehow limited to straight lines. In fact it is In this chapter I’ll present the fundamental concepts behind linear algebra including: •
Scalars and vectors (the elements of linear algebra).
•
The terms and notation of linear algebra.
•
Vector diagrams.
•
Mathematical operations and algebraic rules that can be applied to scalar and vector quantities.
In the next chapter we will see how you can use linear algebra to develop a suite of resources that will help you in your GDL programming.
17.1 Scalars and Vectors The basic entities of Linear Algebra are scalars and vectors. This is an extension to regular algebra, in which each variables holds a single (scalar) value.
essential for working with any geometrical form.
254 Chapter 17: Linear Algebra 101 - Scalars and Vectors
17.1.1
Scalars
As in any form of algebra, a variable like x, an expression such as 3a + 4b, or a constant like 7.65, can be used to indicate a single numeric value. In linear algebra, such a value is referred to as a scalar quantity. There is a reason for using the term scalar, but we’ll get to that later when we discuss Scalar Multiplication.
17.1.2
Vectors
The term vector is used, in the most general sense, to refer to an ordered list of related values. Each member of the list may itself be an expression or a constant. The ordered pair used to describe an (x, y) co-ordinate in graphing (co-ordinate geometry) is one example of a vector. For example, (2, -3) is a vector. In 3D geometry, vectors are frequently used to define position and direction.
17.1.3
Position Vectors
Imagine an ant located at a point in a rectangular x, y, z co-ordinate system. Her position vector contains three values, namely the x, y and z co-ordinates of her position. For example, if the ant is located at co-ordinates (x, y, z) = (1, 3, -2), then her position vector can be written (1, 3, -2). For the moment (and in practice), it’s as simple as that. Position vectors and co-ordinate sets are one and the same. When we discuss Vector Components, we’ll modify the way we think of position vectors a little, but the modification won’t have any real practical impact.
17.1.4
Direction Vectors
Imagine that our friendly ant from the previous section is located at a point in a rectangular x, y, z co-ordinate system. Let’s say that she’s currently situated at the point (-1, 0, 2). The ant runs in a straight line for 1 unit, so that she is now located at a different point. On inspection we find that she is now situated at the point (1.707, 0.707, 2.000). You can measure the change in her position, which is the direct result of the direction and distance (in this case approximately 1 unit – you can check this by using Pythagoras’ theorem) in which she traveled. In our example, she could have reached the same final point if she had run 0.707 units along the x-direction, then 0.707 units along the y-direction, and 0 units in the z-direction. We can write a direction vector that describes the ant’s direction of travel as (0.707, 0.707, 0.000). The three values contained in a direction vector define the change in co-ordinates that occurs when our ant travels exactly one unit in a given direction. If you’re not quite sure what I’m getting at, the following paragraphs on Vector Components should help.
Chapter 17: Linear Algebra 101 - Vector Components 255
17.2 Vector Components We’ve seen that the values in geometric vectors seem to represent different things depending on their context. In the case of position vectors, the values represent co-ordinate values. In the case of direction vectors, the values represent changes in the co-ordinate values. It turns out that even in the case of position vectors, the values represent a change in co-ordinate values. We can think of a position vector as describing a change in position, given the origin as a start point. Any geometric vector, then, is a set of three values. The first value describes a change in the x co-ordinate. The second value describes a change in the y co-ordinate. The third value describes a change in the z co-ordinate. We will use the term component to mean a change in a co-ordinate value. Thus, the change in the x co-ordinate is termed the x-component, and so on.
17.3 Vector Notation In algebra, variables like x and y represent numeric quantities (scalars). In linear algebra, we also use variables to represent vector quantities. To distinguish between vector variables and ordinary variables, we will write vector variables with an underscore. Thus, u and v represent vectors, while u and v represent scalars. Printed texts often use boldface type to represent a vector quantity, but when solving problems using pen and paper, it’s more practical to use an underscored character. When you want to refer to a component of a vector, use a subscript index. For example, the x-, y- and z-components of geometric vector v can be written as v x , v y and v z .
vx v = vy v z For example, a change in position of one unit in the x-direction can be written as:
1 0 0 In this example, the y and z components are both zero, since there is no change in these values.
256 Chapter 17: Linear Algebra 101 - Vector Diagrams
17.4 Vector Diagrams Visualizing a mathematical problem is often the first step towards solving it. We use vector diagrams to help us visualize problems involving vectors. In a vector diagram, each vector is represented by an arrow (figure 224). The arrow has a length (the blunt end) and is pointing in a clearly defined direction. The position of the arrow is usually arbitrary, although by definition the start point of a position vector is always the origin of the axis system (figure 225). It is actually quite difficult to draw an accurate vector diagram representing a 3D problem Figure 224 – Visualize a on a 2D piece of paper. However, the sort of problems we need to solve using vector vector in 3-space as an arrow diagrams tend to be rather general, as we can leave the number-crunching to the computer. with length and direction.
For example, we might want to work out a way to calculate the distance between a point and a plane. We don’t have to produce a scale drawing of that specific plane and point, so as to calculate a numeric value for the distance between them. Rather, we draw a representative ‘general’ plane and a representative ‘general’ point. Our vector diagrams are sketches rather than scale drawings. They help us visualize the problem, and the steps required to arrive at a solution.
17.5 Length of a Vector The length of a vector is also referred to as the vector’s magnitude. This term is helpful Figure 225 – By definition a when working with vectors in fields other than geometry, as in many applications the term ‘position vector’ is anchored at length can be misleading or meaningless. In geometry, the terms length, distance and the origin. magnitude can be used somewhat interchangeably. Given the x, y, z components of a vector, we can use Pythagoras’ theorem to calculate its length (figure 226) (written v );
vx v = vy = vz
vx + v y + vz 2
2
2
Figure 226 – The length of a vector can be calculated using Pythagoras’ theorem.
Chapter 17: Linear Algebra 101 - Equivalent Vectors 257
17.6 Equivalent Vectors In the strictest sense, a vector has no fixed start position. Its start point is arbitrary. You can see this if you consider its component form. The vector is completely defined by its components, which represent a change in co-ordinate value. None of the component data specifies the co-ordinates of a start point. Figure 227 – Two vectors are said to be ‘equivalent’ if their
We will often use the concept of a vector that starts at a directions and lengths are equal. This means that for two vectors particular point. This is a useful tool for visualization. When we use this visualization technique, however, you should be aware that any two vectors are equivalent, provided that their lengths and directions are identical.
to be equivalent, their component values must match.
In other words, two vectors are equivalent if their component values are equal (figure 227).
17.7 Adding Vectors To see how we should go about adding two vectors, let’s first consider a graphical interpretation of adding two numbers, then see how it naturally extends to vectors.
17.7.1
Visualizing Scalar Addition
We can add two numbers graphically on a number line (figure 228). First, we take a line segment whose length represents the first number, and lay it on the number line. Then we take a line segment whose length represents the second number. We lay the second line segment so that it starts where the first ends. Together, the two line segments form a third, resultant line Figure 228 – We can use a number line to visualize the process of segment whose length represents the sum of the two original adding two scalars. numbers.
258 Chapter 17: Linear Algebra 101 - Adding Vectors
17.7.2
Visualizing Vector Addition
To add two vectors graphically, place the tail (start point) of the first vector on an arbitrary point (figure 229). Then place the tail of the second vector onto the head (end point) of the first. Together, the two vectors define a third, resultant vector that starts on the tail of the first vector and ends at the head of the second. This third vector represents the sum of the two original vectors.
17.7.3
Vector Components as Vector Sums
Figure 229 – Vector addition can be visualized in a similar way to scalar addition.
We can visualize a vector as the vector sum of its components. Imagine that each component is itself a vector running along the x, y or z axis. Adding the three component vectors results in our original vector (figure 230). When we add two vectors, you can see how the components add together. Because the x-component vectors are in line, they add together like numbers on a number line. Similarly the y-components add together as scalars, as do the z-components.
17.7.4
Calculate a Vector Sum
Figure 230 – Any vector can be thought of as the sum of component vectors.
In component form, then,, we can simply add each component of the first vector to the corresponding component of the second vector (figure 231).
u x vx u + v = u y + vy = u v z z
u x + vx u + v y y u +v z z
For example, let’s use vector components to add the two vectors (10, 6, 3) and (6, 4, -10).
10 6 10 + 6 16 6 + 4 = 6 + 4 = 10 3 −10 3 + ( −10 ) −7
Figure 231 – Adding two vectors can be achieved by adding the components of the two vectors.
Chapter 17: Linear Algebra 101 - Scalar Multiplication 259
17.8 Scalar Multiplication Scalar multiplication is the process of multiplying a vector by a scalar.
17.8.1
Visualize the Operation
Multiplying a vector by a scalar has the effect of stretching or squashing the vector. The vector’s length changes, but its direction remains constant. In geometric terms, the vector is scaled by the value of the scalar. For example, the equation v = 3 u states that vectors u and v point in the same Figure 232 – Vector v has the direction, and that the length of vector v is three times the length of vector u (figure same direction as vector u but is 3 times longer. 232). Multiplication by a negative scalar has the result of flipping the vector to make it point in the opposite direction. For example, the equation v = −3 u states that vectors u and v point in opposite directions, and that the length of vector v is three times the length of vector u (figure 233). Multiplying a vector by a scalar quantity has the effect of stretching or scaling the vector Figure 233 – Vector v is pointing – hence the term scalar. in a direction opposite to vector u
17.8.2
Calculate the Vector Components
and is 3 times longer.
To stretch or squash a vector while maintaining its direction, we must apply the stretch or squash to each of its components. If we want the resulting vector to be three times longer than the original vector, then we must multiply each of the original vector’s components by 3 (figure 234). In general, to multiply the vector u by scalar t , we simply multiply each component of
u by t .
ux tu= t u y = u z
t ⋅ ux t ⋅u y t ⋅u z
Figure 234 – To scale a vector, all of the components must scale by the same amount.
260 Chapter 17: Linear Algebra 101 - Unit and Zero Vectors
1 For example, given vector u = 2 we calculate 5u as −1 1 5 ×1 5 = 10 5u = 5 2 = 5 × 2 −1 5 × ( −1) −5
17.9 Unit and Zero Vectors Two important concepts that we will use frequently are unit and zero vectors.
17.9.1
Zero Vectors
A vector of zero length is known as (… wait for it …) a zero vector. It has no Figure 235 – To convert a vector v to a unit vector direction. All of its components are zero. scale by the inverse of the length (i.e. divide each
17.9.2
Unit Vectors
If a vector is one unit long, we call it a unit vector. Later, you’ll see that unit vectors are quite useful in certain calculations. A direction vector is one example of a unit vector, and we’ll be using direction vectors frequently. Sometimes we use the term unit direction vector to stress the point (and to remind ourselves) that it is of unit length. We use a hat symbol in place of an underscore to denote a unit vector. Thus, vˆ (vee hat) denotes a unit vector, while v is a regular vector. To calculate the unit vector vˆ that points in the same direction as vector
v , we need to scale v to unit length. The original length of v is v , so if we scale the vector by exactly 1 unit (figure 235).
1 v
, its length will be stretched or squashed to
component value by the length of the original vector).
Chapter 17: Linear Algebra 101 - Cross Product 261
vˆ =
1
v
v For example, if the original vector is 3 units long, we should scale it to 1/3 of its original length. Similarly, if the original vector is ½ a unit in length, we need to scale it to twice its original length. In the earlier section Length of a Vector, we saw that v can be calculated using Pythagoras’ theorem. Try it Yourself: Calculate a Unit Vector
1 For example, let’s find the unit vector that points in the same direction as vector v = 2 . −1 To solve this problem, we first find the length v of vector v .
= v
(1) + (2) + ( −= 1) 2
2
2
6
Now we multiply the original vector by this value to get the unit vector.
= vˆ
1 0.408 2 ≈ 0.816 6 −1 -0.408
1
The length of this vector is 1 unit, and it points in the same direction as the original vector.
17.10
Cross Product
The cross product operates on two vectors to produce a resultant vector that is perpendicular (normal) to both of the two original vectors.
17.10.1 Visualizing the Cross Product The length (magnitude) of the normal vector is equal to the area of the parallelogram defined by the two vectors – i.e. u v sin θ where θ is the angle between the vectors. Actually, the cross product is similar to regular multiplication. Regular multiplication can be visualized as a rectangle area calculation. The product of two
Figure 236 – The result of a cross-product is a vector perpendicular to, amd with length equal to the area subtended by the original vectors.
262 Chapter 17: Linear Algebra 101 - Cross Product numbers a and b is equal to the area of a rectangle of length a and breadth b. The main difference between this and the vector cross product, of course, is that the cross product results in a normal vector. There are two possible directions that we could give the normal vector. By convention (and by definition), the direction of the normal is right-handed. Use the ‘Thumbs Up’ method to predict the direction of the normal vector. With a flat palm, point the fingers of your right hand in the direction of the first vector (figure 237).
Figure 237 – Point your palm & fingers
Keeping your palm pointing along the first vector, bend your fingers so the tips along the direction of the first vector. now point along the second vector (if you have unusually flexible fingers, remember to bend them towards the palm and keep them straight) (figure 238). You will find it necessary to rotate your wrist in order to achieve this arrangement. Put your thumb up (it should now be at right angles to your fingers, pointing away from the palm). This will leave your thumb pointing in the direction of the resulting vector.
17.10.2 How to Calculate the Cross Product The cross product of two vectors u × v can be calculated as follows.
u x vx = u y × vy = u v z z
u y vz − u z v y u z vx − u x vz u v − u v x y y x
Figure 238 – Rotate your wrist as necessary, and bend your fingers at the u×v first knuckle so they point along the second vector. This will leave yur thumb pointing in the direction of the crossThe cool thing about this is that no angles or magnitudes are actually required to product.
calculate a cross product. In fact, only 3 computations (two multiplications and a subtraction) are required for each component of the resulting vector.
17.10.3 A Way to Remember the Cross Product Rather than trying to remember the complete cross-product formula, it’s much easier to remember a rule for calculating just one of the components. To calculate a component value for the cross product, multiply the next component of the first factor with the remaining component of the second factor. Then subtract the product of the next component of the second factor and the remaining component
Chapter 17: Linear Algebra 101 - Cross Product 263 of the first factor. Thus, to calculate the x component of w= u × v , we multiply u y ⋅ vz and subtract v y ⋅ u z .
wx u x vx u y vz − u z v y w = u ×v = u v − u v y y y z x x z w u v u v − u v z z z x y y x Similarly, to calculate the y component of w we multiply u z ⋅ vx and subtract vz ⋅ u x .
wx u x vx u y vz − u z v y w = u ×v = u v − u v y y y z x x z w u v u v − u v z z z x y y x The z-component of w is a mixture of the x and y components of u and v .
wx u x vx u y vz − u z v y w = u ×v = u v − u v y y y z x x z w u v u v − u v z z z x y y x 17.10.4 Calculate the Unit Normal Vector Generally speaking, when we talk about a normal vector, we have in mind a direction vector. We saw earlier that direction vectors must be of unit length. So while it is easy enough to calculate a general normal vector, a bit more work is required to get a unit normal vector. To get our unit normal vector, we must scale the normal vector until it is one unit long. In other words, we must scale the original normal vector by the inverse of its magnitude.
1 1 For example, consider the two vectors u = 0 and v = 1 0
2
2 . 0
Before we calculate the cross product, let’s think about the two vectors. Because both vectors lie in the x-y plane, we
0 already know that the normal vector must point in the z-direction. Thus, the unit normal to u and v must be 0 , or 1
264 Chapter 17: Linear Algebra 101 - Perpendicular Vector in 2D
0 possibly 0 . −1 Now, let’s use the cross product to find a normal vector.
n =u×v
1 0.707 ( 0 )( 0 ) − ( 0 )( 0.707 ) 0 ( 0 )( 0.707 ) − (1)( 0 ) = 0 = 0 × 0.707 = 0 0 1 0.707 − 0 0.707 0.707 ( )( ) ( )( )
The direction of the cross product is in the z-direction, but the length is obviously not one unit. In fact, the length of our normal vector is 0.707 units. We must divide each component by the vector’s length to convert it into a unit normal vector.
nˆ
0 1 = 0 0.707 0.707
0 0 1
There are cases where we don’t need to go the extra mile and calculate a unit normal vector. If you come across such a case, then by all means leave out the extra calculation.
17.10.5 Parallel and Opposite Vectors The cross product of parallel or anti-parallel vectors results in the zero vector. You can see that this must happen, because the area of the parallelogram tends to zero as the angle between the vectors tends to zero. Actually there are an infinite number of vectors that are perpendicular to the two parallel vectors. Selecting the most appropriate becomes a matter of choice.
17.10.6 Cross Product of Perpendicular Unit Vectors There is one case in which the cross product of two unit vectors yields a unit normal vector directly, without having to scale it. When the two original unit vectors are perpendicular, then the area of the parallelogram is one unit, and the resulting cross product is one unit also.
17.11
Perpendicular Vector in 2D
In 2D, all vectors lie in the x-y plane, and are therefore perpendicular to the z-axis. Given a vector u , we can use the cross
Chapter 17: Linear Algebra 101 - Dot Product 265 product of zˆ and u to find a vector v in the x-y plane that is perpendicular to u , i.e. v = zˆ × u . We can calculate the components of
v v v
x
y
z
v using the cross product.
u 0 u ⋅ 1 − 0 ⋅ 0 −u =− u × 0 =− 0 ⋅ 0 − u ⋅ 1 = u 0 1 u ⋅0 − u ⋅0 0 x
y
y
x
x
y
x
y
In practice (figure 239) this simply means that, given a vector u , we can define a perpendicular vector v simply by swapping the x and y co-ordinates, and changing the sign of the x-co-ordinate,
v −u v = u . x
y
x
For example, the vectors
17.12
Figure 239 – Find a vector perpendicular to u by calculating the cross-product of u and (0, 0, 1).
y
3 −1 and are perpendicular. 1 3
Dot Product
It is often very useful to find the projection of one vector onto another. The term projection is easy to picture, but difficult to describe in words. Imagine two vectors u and v that share the same start point, but are pointing in different directions. Now imagine that, starting at the tip of vector v , we draw a line that strikes vector u at right angles. The point at which our line strikes vector u is the projection of v onto u (figure Figure 240 – The dot product is the length of the projection of one vector onto the other. 240). As we’ll see later, knowing the projection of one vector onto another is very useful if we want to perform a change of axes. The dot product operator can be used to return the projection of one vector onto another, or to calculate the angle between two vectors. The dot product of two vectors u and v is written u ⋅ v . By definition, u ⋅ v = u v cos θ where θ is the angle between the
266 Chapter 17: Linear Algebra 101 - Equation of a Line two vectors. If the angle between the two vectors is zero, the dot product gives the same result as the regular product of the lengths of the two vectors. In practice, we can calculate the dot product of two vectors u and v can be calculated as,
u ⋅ v= u x v x + u y v y + u z v z 20 For example,
2 −1 1 ⋅ 2 =( 2 )( −1) + (1)( 2 ) + ( −4 )( 3 ) =−12 −4 3 This gives rise to two useful results. 1.
Since it is so easy to calculate the dot product, the angle between any two vectors can be calculated to be;
cos θ=
u⋅v
= uˆ ⋅ vˆ
u v 2.
The dot product of two vectors can be used to find the projection of one vector onto another. The projection of vector u onto vector v is;
(u
cos θ )= vˆ
( u ⋅ vˆ ) vˆ
These results might not seem very exciting, but you will see later how they can be used in practice to solve geometric problems that would otherwise be quite difficult.
17.13
Equation of a Line
A line can be defined using the position vector p of a point on the line, and a direction vector uˆ pointing along the line (figure 241). Any point x on the line can be expressed as the sum of the position vector and some scalar multiple of the unit direction vector.
20
Figure 241 – Think of a line as a position vector and a direction vector.
If we calculate the dot product of two unit vectors in 2D, we can see how this simplifies to the trig identity
= cos (α − β ) cos (α ) cos ( β ) + sin (α ) sin ( β ) .
Chapter 17: Linear Algebra 101 - Equation of a Plane 267
x= p + tuˆ
x Here I’ve use the vector x to denote a general position vector y . z
17.14
Equation of a Plane
A plane is a set of points that can be defined by the sum of two vectors. The first vector is the position vector to a given point on the plane. The second is an arbitrary vector that lies in the plane. The first vector is fixed, but the second is restricted only by the condition that it must lie in the plane. In other words, the second vector must be perpendicular to the plane’s normal.
( x − p) × n = 0 This is a reasonable test for whether a point lies in the plane, and allows us to intersect a line with a plane.
18 Linear Algebra Applications In this chapter we’ll consider how linear algebra can be used to solve real-life problems that arise frequently when working with GDL. These re-usable sub-routines will provide you with a toolbox of resources that share a consistent look and feel. You can plug these into any GDL script. More importantly, you will see how linear algebra can be used to solve other problems you will come across as you work. Each of the sub-routines has been assigned a numeric identifier. This is intended to provide scope to the variables used by the sub-routine, as discussed in chapter 9 Macros and Sub-Routines.
18.1 Does a Point Lie on a Line Segment? Given a point in 3-space, and a line segment defined by two end points, we need a way to decide whether the point lies on the line segment.
18.1.1
Solving the Problem
Imagine a line segment (of non-zero length L) that starts at the co-ordinates (x1, y1, z1) and ends at the co-ordinates (x2, y2, z2). In the preceding chapter we saw that any point on the line containing this segment can be expressed as the position vector of the start point, plus a scalar multiple t of a unit direction vector. To find the direction vector, first find the vector joining the two points u = (x2 – x1, y2 – y1, z2 – z1), then divide by its length to produce a unit vector.
L=
ux + u y + uz 2
2
2
uˆ = u L We can write:
x = y z
x1 uˆ x y + t uˆ 1 y z uˆ 1 z
where (x, y, z) is the position vector (co-ordinates) of any point on the line. If we begin at the position vector of the start point, and move zero units along the direction vector (t = 0), the value of (x, y, z) is simply (x1, y1, z1). In other words, a value of t = 0 corresponds to the start point of the line segment.
Chapter 18: Linear Algebra Applications - Does a Point Lie on a Line Segment? 269 Similarly, when we move L units along the direction vector uˆ , we arrive at the end point of the line segment. A value of t = L corresponds to the end point of the line segment. Any point that lies between the end points of the line segment has a value of t that lies somewhere between these two values, i.e. 0 ≤ t ≤ L . Now imagine we are given a point with co-ordinates (xi, yi, zi). We are asked to test whether this point lies on the line segment. Effectively, we are being asked to test two things:
xi x1 x2 − x1 1. Is there some value of t that satisfies the equation yi = y1 + t y2 − y1 ? z z z −z i 1 2 1 2.
If so, what is that value of t, and does it lie between 0 and L?
18.1.2
Check whether a value for t exists
Our task is to find a value of t for which our vector equation is satisfied. We can treat the equation as three independent equations in components x, y and z.
xi =x1 + t ( x2 − x1 ) yi =y1 + t ( y2 − y1 ) zi =z1 + t ( z 2 − z1 ) We can solve each of these equations for t. A little manipulation yields the following three solutions.
t= t= t=
xi − x1 x2 − x1 yi − y1 y2 − y1 zi − z1 z 2 − z1
In order for the point to lie on the line that contains our line segment, the three solutions for t must all be equal. That is, for the point to lie on the line, it is necessary that:
270 Chapter 18: Linear Algebra Applications - Does a Point Lie on a Line Segment?
t =
xi − x1 = x2 − x1
yi − y1 z − z1 = i y2 − y1 z 2 − z1
We can further re-arrange the equations to remove the fractions. This yields the following three tests.
( xi − x1 )( y2 − y1 ) = ( yi − y1 )( x2 − x1 ) ( yi − y1 )( z 2 − z1 ) = ( zi − z1 )( y2 − y1 ) ( zi − z1 )( x2 − x1 ) = ( xi − x1 )( z 2 − z1 ) If all three equations are satisfied, we can at least say that the point lies on the line containing our line segment (though it is not necessarily contained between the end points of the line segment). To test this condition, we simply substitute the start point co-ordinate values for x1, y1 and z1, and the test point co-ordinate values for xi, yi and zi.
18.1.3
Test whether t lies between 0 and L
Having performed the test, we know whether or not the point lies on the line. We now need to find a numeric value for t so as to check whether t lies between 0 and L – in other words, does it lie on the line segment? Here I would like to add a note of caution. We could arbitrarily choose one of the three component equations to calculate t. The worst possible choice would be to choose an equation with a denominator equal to zero. To avoid this, and to minimize round-off error, we will use the equation with the denominator of greatest magnitude.
18.1.4
A GDL Sub-Routine
A sub-routine to answer the question as to whether a point lies on a line segment is given below. "4 Does a Point Lie on a Line Segment?": !Input: !
- a line segment (x1~4, y1~4, z1~4) - (x2~4, y2~4, z2~4).
!
- a point (xi~4, yi~4, zi~4) if getValues then x1~4 = get(1): y1~4 = get(1): z1~4 = get(1) x2~4 = get(1): y2~4 = get(1): z2~4 = get(1) xi~4 = get(1): yi~4 = get(1): zi~4 = get(1) getValues = 0 endif
!Return: ! !
A flag lineContainsPoint~4. The returned value is set to: 0 - the point does not lie on the line containing the line segment
Chapter 18: Linear Algebra Applications - Does a Point Lie on a Line Segment? 271 !
1 - the point lies on the line but outside the segment
!
2 - the point lies on the start point
!
3 - the point lies on the end point
!
4 - the point lies between the start and end points
!Calculation: !Calculate the direction vector of the line segment ux~4 = x2~4 - x1~4 uy~4 = y2~4 - y1~4 uz~4 = z2~4 - z1~4 L~4 = sqr(ux~4^2 + uy~4^2 + uz~4^2) ux~4 = ux~4/L~4 uy~4 = uy~4/L~4 uz~4 = uz~4/L~4 !Calculate the vector joining the start point with the test point vx~4 = xi~4 - x1~4 vy~4 = yi~4 - y1~4 vz~4 = zi~4 - z1~4 !Test whether the points lie on the line containing the line segment onLine~4 = 0 if abs(ux~4*vy~4 - uy~4*vx~4) < tol then if abs(uy~4*vz~4 - uz~4*vy~4) < tol then if abs(uz~4*vx~4 - ux~4*vz~4) < tol then onLine~4 = 1 endif endif endif !Solve for t using the greatest denominator if onLine~4 then !Choose the best solution to use !By default use the equation in x bestNumerator~4 = vx~4 bestDenominator~4 = ux~4 !The equation in y may yield a better result if abs(uy~4) > abs(bestDenominator~4) then bestNumerator~4 = vy~4 bestDenominator~4 = uy~4
272 Chapter 18: Linear Algebra Applications - Does a Point Lie on a Line Segment? endif !The equation in z may yield a better result if abs(uz~4) > abs(bestDenominator~4) then bestNumerator~4 = vz~4 bestDenominator~4 = uz~4 endif !Calculate t using the best equation t~4 = bestNumerator~4/bestDenominator~4 endif !Return the appropriate value if onLine~4 then !Check if the point lies on the line segment if t~4 > -tol and t~4 < L~4 + tol then if t~4 > tol and t~4 < L~4 - tol then !The point lies strictly between the start and end points lineContainsPoint~4 = ~4 else if t~4 < 0.5 then !The point lies on the start point lineContainsPoint~4 = 2 else !The point lies on the end point lineContainsPoint~4 = 3 endif endif else !The point lies on the line but outside the segment lineContainsPoint~4 = 1 endif else !The point does not lie on the line containing the line segment lineContainsPoint~4 = 0 endif return
Chapter 18: Linear Algebra Applications - A Test for Parallel Lines 273
18.2 A Test for Parallel Lines Parallel lines are a pest. They don’t intersect neatly at a point, and we can’t even find a single pair of points to mark their closest approach. They just carry on for ever, never getting any closer together and basically ignoring one another. Later in this chapter we will develop algorithms that, given two lines, will calculate their intersection point, find their points of closest approach, and construct a mutually perpendicular vector. These algorithms will work for any pair of lines, provided they are not parallel. Since we are forced to treat parallel lines as a special case, it is important that we have a test for them.
18.2.1
Check for Parallel Lines using the Dot Product
Figure 242 – As two direction vectors become more nearly parallel, their dot product approaches the value 1.
To check whether two lines are parallel, we could calculate the angle between them. If the angle is 0º, or a multiple of 180º, the lines are parallel. We can use the dot product of the direction vectors to calculate the angle between two lines. If the dot product is 1 or -1, then the lines are parallel. The following re-usable GDL sub-routine takes two line segments, defined by their start and end points, and checks whether they are parallel. "3 Check Parallel": !Input: ! !
- Given the differences between start & end points of two line segments (i.e. vectors) (ux~3, uy~3, uz~3) and (vx~3, vy~3, vz~3) if getValues then ux~3 = get(1): uy~3 = get(1): uz~3 = get(1) vx~3 = get(1): vy~3 = get(1): vz~3 = get(1) getValues = 0 endif
!Return: !
- Are the two line segments parallel? = parallelLines~3
!Calculation: !Calculate the lengths of the line segments uL~3 = sqr(ux~3^2 + uy~3^2 + uz~3^2)
274 Chapter 18: Linear Algebra Applications - A Test for Parallel Lines vL~3 = sqr(vx~3^2 + vy~3^2 + vz~3^2) !Calculate the dot product of the unit direction vectors dotProduct~3 = ux~3*vx~3 + uy~3*vy~3 + uz~3*vz~3 dotProduct~3 = abs(dotProduct~3/(uL~3*vL~3)) if abs(dotProduct~3 - 1) < tol then parallelLines~3 = 1 else parallelLines~3 = 0 endif return
Note that, in this script fragment, we didn’t scale the direction vectors to unit length. Since all we needed was the dot product, it was more efficient to divide the product by the two lengths than to scale each component. The above algorithm does not distinguish between parallel and anti-parallel lines. If the distinction is required, we can make a small modification to provide the extra information. Rather than creating a variation of the sub-routine, we’ll use a flag checkAntiParallel and update the script as follows. "3 Check Parallel or AntiParallel": !Input: ! !
- Given the differences between start & end points of two line segments (i.e. vectors) (ux~3, uy~3, uz~3) and (vx~3, vy~3, vz~3) if getValues then ux~3 = get(1): uy~3 = get(1): uz~3 = get(1) vx~3 = get(1): vy~3 = get(1): vz~3 = get(1) getValues = 0 endif
!Flags: !
- Check for anti-parallel lines? = checkAntiParallel~3
!Return: !
- Are the two line segments parallel? parallelLines~3 = -1, 0, 1
!Calculation: !Calculate the lengths of the line segments uL~3 = sqr(ux~3^2 + uy~3^2 + uz~3^2) vL~3 = sqr(vx~3^2 + vy~3^2 + vz~3^2) !Calculate the dot product of the unit direction vectors dotProduct~3 = ux~3*vx~3 + uy~3*vy~3 + uz~3*vz~3 if checkAntiParallel~ 3 then dotProduct~3 = dotProduct~3/(uL~3*vL~3)
Chapter 18: Linear Algebra Applications - A Test for Parallel Lines 275 checkAntiParallel~3 = 0 else dotProduct~3 = abs(dotProduct~3/(uL~3*vL~3)) endif if abs(abs(dotProduct~3) - 1) < tol then parallelLines~3 = sgn(dotProduct~3) else parallelLines~3 = 0 endif return
18.2.2
Check for Parallel Lines using the Cross Product
We could also use the cross product to check for parallel lines.
Figure 243 – As two direction vectors become more nearly parallel, their cross-product approaches the value 0.
If the lines are parallel, then the cross product will be the zero vector. In other words, each component of the cross product will be zero. Note that, unlike the case of the dot product test, we don’t have to use unit vectors. "2 Check Parallel": !Input: ! !
- The differences between the start and end points of two line segments (i.e. vectors) (ux~2, uy~2, uz~2) and (vx~2, vy~2, vz~2) if getValues then ux~2 = get(1): uy~2 = get(1): uz~2 = get(1) vx~2 = get(1): vy~2 = get(1): vz~2 = get(1) getValues = 0 endif
!Return: !
- Are the line segments parallel? = parallelLines~2
!Calculation: !Calculate the cross product wx~2 = uy~2vz~2 - uz~2*vy~2
276 Chapter 18: Linear Algebra Applications - Intersection of Two Lines in 2D wy~2 = uz~2*vx~2 - ux~2*vz~2 wz~2 = ux~2*vy~2 - uy~2*vx~2 !Decide whether the lines are parallel if abs(wx~2) < tol and abs(wy~2) < tol and abs(wz~2) < tol then parallelLines~2 = 1 else parallelLines~2 = 0 endif return
Evidently this is the more efficient of the two algorithms. It requires only half as many operations as the first method. It cannot differentiate between parallel and anti-parallel vectors, but that is not usually a concern.
18.3 Intersection of Two Lines in 2D Next we’ll use our linear algebra skills to find the point of intersection of two lines in the x, y plane. Each line may be defined by two points, or by a position vector p and a direction vector u . In this case, we can break our rule that direction vectors should be of unit length. In component notation, we can write two line equations:
x x11 u1 x x11 x12 − x11 y = y + s u = y + s y − y 11 1 y 11 12 11 x x2 u2 x x21 x22 − x21 y = y + t u = y + t y − y 2 2 y 21 22 21 At the point of intersection the left hand sides of the two equations are equal, so we can equate the right hand sides of the two equations.
x11 u1 x x21 u2 x y + s u = y + t u 11 1 y 21 2 y This leads to a system of equations in s and t.
x11 + s ⋅ u1 x = x21 + t ⋅ u 2 x y11 + s ⋅ u1 y= y21 + t ⋅ u 2 y If we re-arrange these equations to make them explicit in s, we can again equate the right hand sides to get an equation in t.
Chapter 18: Linear Algebra Applications - Intersection of Two Lines in 2D 277
= s
x21 − x11
+t⋅
u1 x
u1 x
y21 − y11
= s
+t⋅
u1 y t=
u2 x u2 y u1 y
u1 x ( y21 − y11 ) − u1 y ( x21 − x11 )
(u
2x
u1 y − u1 x u 2 y )
You may recognize the denominator as the z-component of the cross-product of the two vectors. If the denominator is zero, then the two lines are parallel and there is no unique point of intersection. A GDL script to calculate the intersection point is given below. "5 Intersection of Two Lines - 2D": !Input: ! ! !
- start points and end points of two line segments (x11~5, y11~5) - (x12~5, y12~5) (x21~5, y21~5) - (x22~5, y22~5) if getValues then x11~5 = get(1): y11~5 = get(1) x12~5 = get(1): y12~5 = get(1) x21~5 = get(1): y21~5 = get(1) x22~5 = get(1): y22~5 = get(1) getValues = 0 endif
!Return: !
- whether the lines are parallel: parallelLines~5
!
- the intersection of two lines: (x~5, y~5)
!Calculation: !Calculate the direction vectors u1x~5 = x12~5 - x11~5 u1y~5 = y12~5 - y11~5 u2x~5 = x22~5 - x21~5 u2y~5 = y22~5 - y21~5 !Check if the lines are parallel and find the denominator D~5 = u2x~5*u1y~5 - u1x~5*u2y~5 if abs(D~5)/(sqr(u1x~5^2 + u1y~5^2)*sqr(u2x~5^2 + u2y~5^2)) < tol then
278 Chapter 18: Linear Algebra Applications - Intersection of Two Lines in 2D parallelLines~5 = 1 else parallelLines~5 = 0 endif !Intersection point of non-parallel lines if not(parallelLines~5) then t~5 = (u1x~5*(y21~5 - y11~5) - u1y~5*(x21~5 - x11~5))/D~5 x~5 = x21~5 + t~5*u2x~5 y~5 = y21~5 + t~5*u2y~5 endif return
Try it Yourself: Intersection of Non-Parallel Lines To see how this works in practice, create a new object. Add parameters with variables x1, y1, x2, y2, x3, y3, x4, y4, and give them some non-trivial values (figure 244). Copy the following GDL code into the 2D Script editing window. tol = 0.00001 !Draw the two line segments line2 x1, y1, x2, y2 line2 x3, y3, x4, y4 !Find the intersection point
Figure 244 – Create a new object with length parameters having variables x1, y1, x2, y2, x3, y3, x4, y4 that represent the start and end points of two line segments.
getValues = 1 put x1, y1, x2, y2, x3, y3, x4, y4 gosub “5 Intersection of Two Lines – 2D” if not(parallelLines~5) then circle2 x~5, y~5, 0.01 endif end "5 Intersection of Two Lines – 2D":
Figure 245 – The intersection point is shown using a circle.
return
Note that in this example we used the getValues variable to tell the sub-routine to get the input values from the stack. We could equally well have explicitly set the input variable values in the main script. The sub-routine allows for both input
Chapter 18: Linear Algebra Applications - Intersect 2 Line Segments in 2D 279 methods. Click on the 2D Full View symbol to see the result (figure 245). Change the parameter values for the line co-ordinates, and you’ll see that the algorithm gets the right answer every time. You can even try trivial line segments, where both points have the same co-ordinates. The algorithm is robust.
18.4 Intersect 2 Line Segments in 2D The above sub-routine is great if you just want to know the intersection point of the line segments. When we consider polygon operations, we’ll see that it can be important to know whether the intersection point is contained by the two line segments, or if it lies outside of their end points. This requires a variation of the original sub-routine. "6 Intersection of Two Line Segments - 2D": !Input: ! ! !
- start points and end points of two line segments (x11~6, y11~6) - (x12~6, y12~6) (x11~6, y11~6) - (x12~6, y12~6) if getValues then x11~6 = get(1): y11~6 = get(1) x12~6 = get(1): y12~6 = get(1) x21~6 = get(1): y21~6 = get(1) x22~6 = get(1): y22~6 = get(1) getValues = 0 endif
!Return: !
- whether the lines are parallel: parallelLines~6
!
- the intersection point of the two lines: (x~6, y~6)
!
- whether the point lies on both line segments: linesIntersect~6
!
- whether the point is contained by the first line segment: lineSplits1~6
!
- whether the point is contained by the 2nd line segment: lineSplits2~6
!Calculation: !Direction vectors (not of unit length) u1x~6 = x12~6 - x11~6 u1y~6 = y12~6 - y11~6 u2x~6 = x22~6 - x21~6 u2y~6 = y22~6 - y21~6
280 Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D !Check if the lines are parallel and find the denominator D~6 = u2x~6*u1y~6 - u1x~6*u2y~6 if abs(D~6)/(sqr(u1x~6^2 + u1y~6^2)*sqr(u2x~6^2 + u2y~6^2)) < tol then linesSplit~6 = 0 lineSplits1~6 = 0 lineSplits2~6 = 0 parallel~6 = 1 else parallel~6 = 0 !The lines intersect - find the intersection point t~6 = (u1x~6*(y21~6 - y11~6) - u1y~6*(x21~6 - x11~6))/D~6 s~6 = (u2x~6*(y21~6 - y11~6) - u2y~6*(x21~6 - x11~6))/D~6 x~6 = x21~6 + t~6*u2x~6 y~6 = y21~6 + t~6*u2y~6 linesIntersect~6 = (t~6 > -tol and t~6 < 1 + tol) and (s~6 > -tol and s~6 < 1 + tol) lineSplits1~6 = linesSplit~6*(t~6 > tol and t~6 < 1 - tol) lineSplits2~6 = linesSplit~6*(s~6 > tol and s~6 < 1 - tol) endif return
To check whether the line segments intersect or are split by the intersection point, we have to test two things. First we test if the intersection point is loosely bracketed by both line segments. This could occur, for example, if the end point of one of the line segments lay on the midpoint of the second. While the intersection point is not strictly bracketed by the first line segment, it is loosely bracketed, so a split is possible on the second line segment. We also have to test whether the intersection point is strictly bounded by the end points of each line segment,. If so, then that line segment is split by the intersection point.
18.5 Intersect a Line Segment and an Arc in 2D Given a line segment defined by a two points ( x1 , y1 ) and ( x2 , y2 ) , and an arc
, yC ) , radius R , and start & end angles α and β , we want to find any intersection points. We must decide whether the intersection Figure 246 – Our aim is to calculate any points lie between the end points of the line segment and within the extents of inter-section points on the arc and line
defined by center
(x
C
Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D 281 the arc (figure 246). Our approach in solving this problem amounts to a change in axes. We want to express the problem in terms of an axis aligned with the direction vector uˆ of the line. We will first calculate the unit direction vector uˆ of the line and a unit vector vˆ perpendicular to uˆ . This provides a frame of reference in which the problem of calculating the intersection points becomes rather easy (figure 247). From the diagram we can see that if we first calculate the value h , we can then use Pythagoras’ theorem to calculate w . Now we can find the intersection Figure 247 – To simplify the problem, we’ll points. Starting at the arc’s center point, we will move a distance h in the change our axis system . direction vˆ . From this new position we can reach the two possible solutions by moving a positive or negative distance along the direction uˆ (figure 248). This method works well, for two reasons. Firstly, it simply identifies all of the special cases. If the absolute value of h is greater than R , then there are no intersection points. If the absolute value of w is zero, there is only one intersection point. Secondly, it doesn’t involve any complicated algebra.
18.5.1
Performing the Calculation
1 Find the line segment’s unit direction vector We’ll use Pythagoras’ theorem to calculate the length of the line segment.
(x
L=
− x1 ) + ( y2 − y1 ) 2
2
2
Having found L , we can construct a unit direction vector for the line segment.
( x2 − x1 ) / L ( y2 − y1 ) / L
uˆ =
2 Calculate a perpendicular vector To find a perpendicular direction vector vˆ , we can use the result we obtained in the section, Perpendicular Vector in 2D.
Figure 248 – If h is greater than R, are no solutions exist. If equal, there is 1 solution.
282 Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D
vˆx −uˆ y vˆ = uˆ y x 3 Calculate the distance h We can find h by calculating the dot product p ⋅ vˆ where p is the vector starting at ( xC , yC ) and ending at ( x1 , y1 ) (figure 249).
h = p ⋅ vˆ =
x1 − xC −u y y − y ⋅ u 1 C x
= u x ( y1 − yC ) − u y ( x1 − xC ) Note that if the length of h is greater than the radius (i.e. h > R ) then the line
Figure 249 – Find h by calculating the
misses the arc completely, and there is no point of intersection. If h is identical to projection of p onto v. the radius ( h − R < tol ) there is exactly one point of intersection. 4 Calculate w In the case where one or more solutions exist, we can use Pythagoras Theorem to calculate w (figure 250).
= w
R −h 2
2
Note that if w is zero (or, in GDL terms, if w < tol) there is only one point of intersection. 5 Find the intersection points We now have all the information we need to calculate the two intersection points ( xL , y L ) and ( xR , y R ) that occur to either side of vector hvˆ (figure 251). Finding these two points is as easy as carrying out two vector additions.
xL xC vx u x y = y + h v − w u L C y y xR xC vx u x y = y + h v + w u R C y y
Figure 250 – Calculate w courtesy of Pythagoras.
Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D 283 6 Test if the points lie on the line segment. We know that the points lie on the line, but do they occur between the end points of the line segment? Let’s start by writing a parametric equation for the straight line.
x x1 u x = +t y y1 u y In this equation, setting t = 0 defines the start point of our line segment, while setting t = L defines the end point. Given a point that we know lies on the line, we simply need to solve one of the equations (either in x or y) for t.
t=
xi − x1
Figure 251 – Once we have calculated h and w, the remaining work is rather straightforward
ux t=
yi − y1 uy
If the calculated value of t lies between 0 and L, then the point lies on the line segment. Which equation should we choose? The worst option would be to choose an equation where the denominator is zero. Ideally, we would use a large value for the denominator. So we’ll adopt the strategy of choosing the equation that has the largest denominator. 7 Test if the points lie on the arc. We know that the calculated intersection points lie on the circumference of the circle, but they may lie outside of the interval defined by the arc’s start and end points. To avoid any confusion, we will use the convention that the angle of an arc’s start point must be numerically less than the angle at its end point. In other words, there is no such thing as an arc that starts at 330º and ends at 30º. Either it starts at 30º and ends at 330º, or it starts at 330º and ends at 390º. If the given end angle is numerically less than the start angle, then, we will always choose to add 360º to the end angle. This is a matter of choice – we could have simply swapped end angles, which would have given a different but equally valid result. The main thing is that we are consistent, so that anyone who uses our sub-routine will know how to input data to achieve the desired outcome. Using this convention, we can easily check if the intersection points correspond to an angle lying between the start and end points. The first step is to add 360 to the end angle if necessary. Then we must calculate the angle θ defined by the center point and our intersection point, ensuring that this too is greater than the arc’s start angle. Then provided θ is less than the end angle, it must be included within the bounds of the arc.
284 Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D
18.5.2
A GDL Sub-Routine
A GDL sub-routine to calculate the intersection of a line segment with an arc might read as follows: "18 Intersection of Line Segment and Arc": !Input: ! - a line segment (x1~18, y1~18) - (x2~18, y2~18) ! - an arc (xC~18, yC~18, R~18, q1~18, q2~18) !Get the data from the stack if getValues then !Line Definition x1~18 = get(1): y1~18 = get(1) x2~18 = get(1): y2~18 = get(1) !Arc Definition xC~18 = get(1): yC~18 = get(1) R~18 = get(1) q1~18 = get(1): q2~18 = get(1) getValues = 0 endif !Return: ! - the number of points of intersection (nSolutions~18) ! – the intersection points (xi~18, yi~18) and (xj~18, yj~18) ! – intersection status of the points with the line segment !
lineIntersectsi~18, lineIntersectsj~18
! – intersection status of the points with the arc !
arcIntersectsi~18, arcIntersectsj~18
!Calculation: !Initialize intersection flags lineIntersectsi~18 = 0 lineIntersectsj~18 = 0 arcIntersectsi~18 = 0 arcIntersectsj~18 = 0 !Check that the arc’s end angle is greater than its start angle if q2~18 < q1~18 + tol then q2~18 = q2~18 + 360 endif !Calculate the direction vector of the line ux~18 = x2~18 – x1~18
Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D 285 uy~18 = y2~18 – y1~18 L~18 = sqr(ux~18^2 + uy~18^2) ux~18 = ux~18/L~18 uy~18 = uy~18/L~18 !Calculate a perpendicular vector vx~18 = -uy~18 vy~18 =
ux~18
!Calculate the closest distance between the center point and the line h~18 = (x1~18 – xC~18)*vx~18 + (y1~18 – yC~18)*vy~18 !Test the number of solutions D~18 = abs(h~18) if D~18 > R~18 + tol/2 then nSolutions~18 = 0 return else if abs(D~18 – R~18) < tol then nSolutions~18 = 1 else nSolutions~18 = 2 endif endif !Calculate the intersection points if nSolutions~18 = 1 then xi~18 = xC~18 + h~18*vx~18 yi~18 = yC~18 + h~18*vy~18 else !Calculate the distance between the closest approach !and the intersection points W~18 = sqr(R~18^2 – h~18^2) !Calculate the intersection points xi~18 = xC~18 + h~18*vx~18 – w~18*ux~18 yi~18 = yC~18 + h~18*vy~18 – w~18*uy~18 xj~18 = xC~18 + h~18*vx~18 + w~18*ux~18 yj~18 = yC~18 + h~18*vy~18 + w~18*uy~18 endif !Check that the intersection points split line segment
286 Chapter 18: Linear Algebra Applications - Intersect a Line Segment and an Arc in 2D ti~18 = ux~18*(xi~18 - x1~18) + uy~18*(yi~18 - y1~18) tj~18 = ux~18*(xj~18 - x1~18) + uy~18*(yj~18 - y1~18) if ti~18 > -tol and ti~18 < L~18 + tol then if ti~18 > tol and ti~18 < L~18 - tol then lineIntersectsi~18 = 2 else lineIntersectsi~18 = 1 endif endif if nSolutions~18 = 2 then if tj~18 > -tol and tj~18 < L~18 + tol then if tj~18 > tol and tj~18 < L~18 - tol then lineIntersectsj~18 = 2 else lineIntersectsj~18 = 1 endif endif endif !Check that the intersection points split the arc !Calculate the angle dx~1 = xi~18 – xC~18 dy~1 = yi~18 – yC~18 gosub "1 Calculate Angle" qi~18 = q~1 if qi~18 < q1~18 - tol then qi~18 = qi~18 + 360 if nSolutions~18 = 2 then dx~1 = xj~18 – xC~18 dy~1 = yj~18 – yC~18 gosub "1 Calculate Angle" qj~18 = q~1 if qj~18 < q1~18 - tol then qj~18 = qj~18 + 360 endif !Check if it lies in the range q10[1] < qi10 < q10[2] if qi~18 < q2~18 + tol then if qi~18 < q2~18 - tol and qi~18 > q1~18 + tol then arcIntersectsi~18 = 2 else
Chapter 18: Linear Algebra Applications - Calculate an Angle 287 arcIntersectsi~18 = 1 endif endif if nSolutions~18 = 2 then if qj~18 < q2~18 + tol then if qj~18 < q2~18 - tol and qi~18 > q1~18 + tol then arcIntersectsj~18 = 2 else arcIntersectsj~18 = 1 endif endif endif return
18.6 Calculate an Angle While this does not really fit into the chapter on Linear Algebra, it is important enough to warrant inclusion at this point. Mathematical angles are always measured counter-clockwise from the x-axis. The problem here is, given a line segment in the x-y plane, to calculate the angle it makes with the x-axis (figure 252). The returned angle should be between 0º and 360º. If the angle is very close to 360º, it should be returned as 0º. Oddly enough, this problem is more involved than it might Figure 252 – Given a vrctor, appear. calculate the angle α measured Given a line segment that starts at point (x1, y1) and ends at point (x2, y2), let
from the horizontal.
Δx = x2 – x1 Δy = y2 – y1 Now the line segment forms a right angle triangle with Δx and Δy as its two perpendicular sides (figure 253). The angle θ is adjacent to Δx and opposite Δy. Our old friend Soh Cah Toa tells us that tan(θ) = Δy / Δx. To solve for θ, we can take the inverse tangent of both sides of the equation.
θ = atn(Δy / Δx) If only it were that simple – but alas, no! There are some special cases to tidy up that could Figure 253 – First calculate Дx and Дy.
288 Chapter 18: Linear Algebra Applications - Calculate an Angle otherwise end up crashing ArchiCAD. For one thing, the dreaded divide by zero error must be guarded against. If Δx is zero (which will be the case for any vertical line segment), the fraction Δy/Δx becomes undefined. This is quite easy to work around, as we know that for vertical lines the angle θ must be either 90º or 270º. If Δy is positive, the line segment is pointing upwards, and we can choose 90º. If Δy is negative, the line segment is pointing straight down, so the angle is 270º. Another consideration is that the graph of tan(θ) repeats itself every 180º. This is a nuisance because without some further test we don’t know whether the angle is, say, 30º or 210º. A simple solution is to add 180º if the value of Δx is negative. The final issue is that if Δy is very small and negative, the result might be very close to 360º. Since 360º is pretty much the same as zero, it would be better to return zero. Taking all the above into consideration, the following GDL sub-routine works well in every case. "1 Calculate Angle": !Input: dx~1, dy~1 if getValues then dx~1 = get(1): dy~1 = get(1) getValues = 0 endif !Return: angle q~1 !Calculation: if abs(dx~1) < tol then if dy~1 > 0 then q~1 = 90 else q~1 = 270 endif else q~1 = atn(dy~1/dx~1) if dx~1 < 0 then q~1 = 180 + q~1 endif q~1 = (360 + q~1)%360 if abs(360 - q~1) < tol then q~1 = 0 endif endif
Chapter 18: Linear Algebra Applications - Intersection of Two Arcs 289 return
Note that this sub-routine uses our constant tol which should be defined at the head of the Master Script. tol = 0.00001
There are two reasons for using a sub-routine to calculate the angle rather than including this block in the main script. First, because it is likely that you will need to calculate angles at more than one place in a given GDL script. Second, because the script block contains multiple lines, and would be clumsy to include in the main script. After all, calculating an angle shouldn’t be a drama, so its good to be able to do so using a couple of lines of script: dx~1 = x2 – x1: dy~1 = y2 – y1 gosub “1 Calculate Angle”
18.7 Intersection of Two Arcs Given two arcs ( xC 1 , yC 1 , R1 , α1 , β1 ) and ( xC 2 , yC 2 , R2 , α 2 , β 2 ) , we wish to find any points of intersection. From the diagram (figure 254), we can see that the two circles from which the arcs are taken will intersect if the distance L between the centers is less than the sum of the radii, i.e.
(x
C1
− xC 2 ) + ( yC 1 − yC 2 ) ≤ ( R1 + R2 ) 2
2
Notice that the line L forms a triangle with radii R1 and R2. We can use the cosine rule to calculate θ1 and θ 2 .
R12 + L2 − R2 2 θ1 = cos 2 R1 L −1
R2 2 + L2 − R12 2 R2 L
θ 2 = cos −1
If we calculate the unit direction vector uˆ that points from
(x
C1
, yC 1 ) to
(x
C2
, yC 2 ) and a perpendicular vector v , we can
express our intersection points as components of uˆ and v .
Figure 254 – To find the intersection points of two arcs, the first thing to consider is whether such points exist. This can be checked by comparing the sum of the arc radii with the separation of the two centers.
290 Chapter 18: Linear Algebra Applications - Intersection of Two Arcs
ux vx x xC 1 y =+ y R1 cos θ1 u ± R1 sin θ1 v C1 y y All that remains is to check if the intersection points are contained in both of the arcs. A GDL script for the complete process is given below. "17 Intersection of Two Arcs": !Input: !
- arc 1 (xC1~17, yC1~17, R1~17, with start & end angles q11~17, q21~17)
!
- arc 2 (xC2~17, yC2~17, R2~17, with start & end angles q12~17, q22~17) if getValues then !Arc 1 Definition xC1~17 = get(1): yC1~17 = get(1): R1~17 = get(1) q11~17 = get(1): q21~17 = get(1) !Arc 2 Definition xC2~17 = get(1): yC2~17 = get(1): R2~17 = get(1) q12~17 = get(1): q22~17 = get(1) endif
!Return: !
- number of intersection points = nSolutions~17
!
- any points of intersection (x1~17, y1~17), (x2~17, y2~17)
!
- a flag to tell whether the points lie on the arcs
!Calculate the direction vector ux~17 = xC2~17 - xC1~17 uy~17 = yC2~17 - yC1~17 L~17 = sqr(ux~17^2 + uy~17^2) ux~17 = ux~17/L~17 uy~17 = uy~17/L~17 !Calculate the angles if abs(R1~17 + R2~17 - L~17) < tol then nSolutions~17 = 1 u~17 = R1~17 x1~17 = xC1~17 + u~17*ux~17 y1~17 = yC1~17 + u~17*uy~17 else if R1~17 + R2~17 + tol > L~17 then !Calculate a perpendicular direction
Chapter 18: Linear Algebra Applications - Intersection of Two Arcs 291 vx~17 = -uy~17 vy~17 = ux~17 !Calculate the angle at the center of the first arc !spanning the height at the circumference q1~17 = acs((R1~17^2 + L~17^2 - R2~17^2)/(2*R1~17*L~17)) !Calculate the vector components to reach the intersection points u~17 = R1~17*cos(q1~17) v~17 = R1~17*sin(q1~17) !Calculate the intersection points x1~17 = xC1~17 + u~17*ux~17 + v~17*vx~17 y1~17 = yC1~17 + u~17*uy~17 + v~17*vy~17 x2~17 = xC1~17 + u~17*ux~17 - v~17*vx~17 y2~17 = yC1~17 + u~17*uy~17 - v~17*vy~17 else nSolutions~17 = 0 u~17 = R1~17 x1~17 = xC1~17 + u~17*ux~17 y1~17 = yC1~17 + u~17*uy~17 endif endif !Check that the intersection points fall within the arc bounds arcIntersects1~17 = 0 arcIntersects2~17 = 0 !First intersection point if nSolutions~17 then dx~1 = x1~17 - xC1~17 dy~1 = y1~17 - yC1~17 gosub "1 Calculate Angle" if q~1 < q11~17 - tol then q~1 = q~1 + 360 endif if q~1 < q2~17 + tol then if q~1 < q2~17 - tol and q~1 > q1~17 + tol then arcIntersects1~17 = 2 else arcIntersects1~17 = 1
292 Chapter 18: Linear Algebra Applications - Equation of a 2D Arc through Three Points endif endif endif !Second intersection point if nSolutions~17 = 2 then dx~1 = x2~17 - xC1~17 dy~1 = y2~17 - yC1~17 gosub "1 Calculate Angle" if q~1 < q11~17 - tol then q~1 = q~1 + 360 endif if q~1 < q2~17 + tol then if q~1 < q2~17 - tol and q~1 > q1~17 + tol then arcIntersects2~17 = 2 else arcIntersects2~17 = 1 endif endif endif return
18.8 Equation of a 2D Arc through Three Points We can use some results of Linear Algebra to develop an algorithm to calculate the center, radius and end angles of an arc that runs through 3 points (figure 255).
18.8.1
Plan a Solution
Our first step in planning a solution is to visualize the problem and plan a solution. Visualizing the problem is as easy as sketching three somewhat arbitrary points with a particular order. By eye we will sketch an arc running through the points. Figure 255 – Our challenge here is to If we join the points with line segments, we have effectively formed two chords of calc-ulate the center point and end the arc. With a little insight we can see that the intersection of the perpendicular angles for an arc that passes through bisectors through the chords will give us our center point (figure 256). Once we three points. have found the center point, the remaining parts of the problem are routine. This approach will form the basis of our solution.
Chapter 18: Linear Algebra Applications - Equation of a 2D Arc through Three Points 293 1.
We will first join points (x1, y1) and (x2, y2) with a vector u1 , and define the perpendicular bisector as a vector v1 starting at the midpoint with a direction perpendicular to u1 . We will similarly define the perpendicular bisector v2 of the vector u2 joining points (x2, y2) and (x3, y3).
2.
To find the center (xC, yC) of the circle, we can calculate the intersection of the perpendicular bisectors vectors v1 and v2 .
3.
Figure 256 – We can construct a solution by forming two chords, and We will use Pythagoras’ theorem to calculate the radius as the distance intersect their perpendicular bisectors.
between the center point and the first point on the perimeter (x1, y1).
4.
Finally, we can use our 1 Calculate Angle sub-routine to find the arc’s start and end angles α and β.
1 Find the perpendicular bisector vectors We will construct the perpendicular bisectors as vectors through the midpoints of the line segments. To find the direction vectors for the bisectors, we can take the direction vectors joining the pairs of points. In the preceding chapter, we saw how to construct a perpendicular vector in 2D. We can thus write the equations of the perpendicular bisectors as:
x x = y y
1
2
2
1
x x = y y
+x
+y
2
2 2
+x
3
2 2
+y 2
3
y + s x
2
1
y +t x
3
2
−y −x
−y −x
1
2
2
3
2 Calculate the arc center At the point of intersection (i.e. the center of the arc), these two equations must give the same value for x and y for some values of s and t.
294 Chapter 18: Linear Algebra Applications - Equation of a 2D Arc through Three Points
x y
1
1
+x
y + s x
2
2 +y
−y
2
−x
1
2
2
x = y
1
2
2
+x
3
2 2
+y 2
3
y +t x
3
2
−y −x
2
3
Separating out the equations in x and y, and making them explicit in s, we get: = s
x −x 3
2( y − y 2
= s
+t
1
y −y 3
)
1
2(x − x 1
1
)
2
(y (y (x (x
+t
−y
3
−y
2
2
1
−x −x
2
1
3
2
) )
) )
Equating the right hand sides, and solving for t, we get: t =
( y − y )( y − y ) − ( x − x )( x − x ) 2 [( y − y )( x − x ) − ( x − x )( y − y ) ] 3
1
3
2
2
1
1
3
2
1
2
1
3
2
2
1
In the above solution for t, the denominator will be zero only if the perpendicular bisectors are parallel. This is the case when all three points are in line. Having found t, we can substitute back into the original equation to find the center point x and y. 3 Calculate the radius Now that we know the center point, we can use Pythagoras’ theorem to calculate the radius as the distance from the center to a point on the perimeter. We will use the first given point as the point on the perimeter, so that R=
(x
1
−x
C
)
2
−(y − y 1
C
)
2
4 Calculate the arc start and end angles We can calculate the arc end angles using the 1 Calculate Angle sub-routine. 5 Ensure the correct part of the arc is used The arc may run either clockwise or counter-clockwise from the start point to the end point. If it is to run clockwise, then we must ensure that the start angle has a greater value than the end angle. If the arc is to run counter-clockwise, we must force the end angle to have the greater value. You may think that this would automatically result from our calculation, but because angles are not uniquely ordered (for example, the angle -30° is the same as 330°), we do have to perform a test and if necessary adjust one of the angle values by 360°.
Chapter 18: Linear Algebra Applications - Equation of a 2D Arc through Three Points 295
18.8.2
A GDL Sub – Routine to Calculate an Arc through 3 Points
In GDL code, this process is very direct, taking only a few lines of code. “15 Arc Through 3 Points”: !Input: !
- three points (x1~15, y1~15), (x2~15, y2~15), (x3~15, y3~15) if getValues then x1~15 = get(1): y1~15 = get(1) x2~15 = get(1): y2~15 = get(1) x3~15 = get(1): y3~15 = get(1) getValues = 0 endif
!Return: !
- the center (xC~15, yC~15)
!
- radius R~15
!
- and end angles q1~15, q2~15
! !
of an arc that passes through three points - a flag isClockwise~15
!Calculation: !1. Intersect the perpendicular bisectors D~15 = 2*((y3~15 - y2~15)*(x1~15 - x2~15) - (x2~15 - x3~15)*(y2~15 - y1~15)) pointsInLine~15 = 1 if abs(D~15) > tol then pointsInLine~15 = 0 t~15 = ((y3~15 - y1~15)*(y2~15 - y1~15) + (x3~15 - x1~15)*(x2~15 - x1~15))/D~15 xC~15 = (x2~15 + x3~15)/2 + t~15*(y3~15 - y2~15) yC~15 = (y2~15 + y3~15)/2 + t~15*(x2~15 - x3~15) endif !2. If the points are in line, return to the main script if pointsInLine~15 then return !3. Calculate the Radius and End Angles !Radius R~15 = sqr((x1~15 - xC~15)^2 + (y1~15 - yC~15)^2) !Start Angle dx~1 = x1~15 - xC~15 dy~1 = y1~15 - yC~15 gosub "1 Calculate Angle"
296 Chapter 18: Linear Algebra Applications - Equation of a 2D Arc through Three Points q1~15 = q~1 !End Angle dx~1 = x3~15 - xC~15 dy~1 = y3~15 - yC~15 gosub "1 Calculate Angle" q2~15 = q~1 !Check if the arc is clockwise or counterclockwise isClockwise~15 = ((x2~15 - x1~15)*(y2~15 + y1~15) + (x3~15 - x2~15)*(y3~15 + y2~15) + (x1~15 - x3~15)*(y1~15 + y3~15) > 0) !Depending on the sense then force either the start or end angle !to be the larger of the two if isClockwise~15 then if q2~15 > q1~15 then q1~15 = q1~15 + 360 endif else if q2~15 < q1~15 then q2~15 = q2~15 + 360 endif endif return
Try it Yourself: Arc through 3 Points Create a new object with a length type parameter xy. Click on the Array button, and add rows and columns to create a 3 x 2 array of values. Each row will be used to store the (x, y) coordinates of a point (figure 257). We will write a script to run an arc through the three points, so you should give the points some sensible default values. Copy the following GDL script into the 2D Script editing window. !Place text to identify each point define style "tx" Arial, 1, 1, 0 style "tx" for i = 1 to 3 text2 xy[i][1], xy[i][2], i
Figure 257 – Create an object as described in the text, and enter values for the co-ordinates of three points.
Chapter 18: Linear Algebra Applications - Equation of a 2D Arc through Three Points 297 next i !Hotspot controls for the three points iHotspot = 0 for i = 1 to 3 add2 xy[i][1], xy[i][2] hotspot2 0, -xy[i][2], iHotspot + 1, xy[i][2], 1 + 128 hotspot2 0, 0, iHotspot + 2, xy[i][2], 2 hotspot2 0, -xy[i][2] - 1, iHotspot + 3, xy[i][2], 3 iHotspot = iHotspot + 3 hotspot2 -xy[i][1], 0, iHotspot + 1, xy[i][1], 1 + 128 hotspot2 0, 0, iHotspot + 2, xy[i][1], 2 hotspot2 -xy[i][1] - 1, 0, iHotspot + 3, xy[i][1], 3 iHotspot = iHotspot + 3 del 1 next i !Calculate the Arc Center and Angles x1~15 = xy[1][1]: y1~15 = xy[1][2] x2~15 = xy[2][1]: y2~15 = xy[2][2] x3~15 = xy[3][1]: y3~15 = xy[3][2] gosub "15 Arc Through 3 Points" !Draw the Arc if pointsInLine01 then line2 x1~15, y1~15, x2~15, y2~15 line2 x2~15, y2~15, x3~15, y3~15 else if isClockwise01 then arc2 xC~15, yC~15, R~15, q2~15, q1~15 else arc2 xC~15, yC~15, R~15, q1~15, q2~15 endif endif end "1 Calculate Angle": (insert the content of the 1 Calculate Angle sub-routine) return “15 Arc Through 3 Points”:
298 Chapter 18: Linear Algebra Applications - Closest Point on a Line (insert the content of the above sub-routine) return
In particular, note the block of code starting with the comment !Calculate the Arc Center and Angles. Because we want to use the algorithm to draw a GDL hotarc or arc, we are also swap the start and end points if the arc is to run in a clockwise direction (figure 258). Save the object as Arc through Three Points, and place an instance of it into the ArchiCAD project. Try moving the points. You will find that no matter where you place the 3 points, the arc will always be drawn correctly between points 1 and 3, and will always pass through point 2. When using this sub-routine, you should be sure to check for the special case where all the points all lie on the same line, or two or more points are superimposed. In such cases Figure 258 – When you run the the sub-routine will fail to return arc data, so you should build in some code to deal with 2D script, it draws the 3 points the situation. with numeric labels, and scribes an arc through them.
18.9 Closest Point on a Line Given a point P and a line L (defined by two points on the line), the problem is to find a point Q on line L that is closest to P.
18.9.1
Visualize the Problem in terms of Vectors
As with any geometrical problem, the first step is to visualize the situation. In this case, the problem can be visualized by sketching a line and a point (figure 259). The line is defined by the position vectors of two points
x1 x2 q1 = y1 and q2 = y2 , z z 1 2 while point P has position vector
Figure 259 – Given a point Pand a line L, find the point Q on L that is closest to P.
Chapter 18: Linear Algebra Applications - Closest Point on a Line 299
xi p = yi . z i We’ll re-draw our original diagram to reflect this insight (figure 260). We also know that the any point on the line is the sum of position vector q1 and some scalar multiple of direction vector uˆ . To solve the problem, then, we can construct the line’s unit direction vector uˆ , and a vector
v= p − q1 . From the diagram (figure 261), you can see that the dot product of these two vectors will yield the distance from the line’s start point at which the closest point occurs. 1.
Calculate the unit direction vector uˆ .
The first step is to calculate the unit direction vector.
x2 − x1 u = q2 − q1 = y2 − y1 z −z 2 1
Figure 260 – Re-draw our diagram to show the point as a position vector.
The magnitude of u is calculated using Pythagoras’ theorem.
u=
ux + u y + uz 2
2
2
The unit direction vector is then:
uˆ =
1
u
u 2.
Calculate a vector v joining (x1, y1, z1) with point P.
The components of the vector joining points Q1 and P are given by:
xi x1 xi − x1 v = p − q1 = yi − y1 = yi − y1 z z z −z i 1 i 1
Figure 261 – By redrawing our diagram to show the line as a position and a direction vector, the solution becomes apparent.
300 Chapter 18: Linear Algebra Applications - Closest Point on a Line 3.
Find the projection.
We will use the dot product v ⋅ uˆ to find the distance of the closest point on the line from point q1 .
D= v ⋅ uˆ 4.
Finally, calculate the closest point.
The final step is to calculate the closest point x on the line, given its distance D from point p .
x= q1 + Duˆ
A GDL Sub-Routine A sub-routine to calculate the point on a line closest to a given point might look like this. “16 Closest Point on a Line”: !Given: ! - a line defined by two points (x1, y1, z1) – (x2, y2, z2) ! - a test point (xi, yi, zi) !Find the point on the line that is closest to the test point if getValues then x1~16 = get(1): y1~16 = get(1): z1~16 = get(1) x2~16 = get(1): y2~16 = get(1): z2~16 = get(1) xi~16 = get(1): yi~16 = get(1): zi~16 = get(1) getValues = 0 endif !1. Calculate vector v, starting at x1, y1, z1 and ending at xi, yi, zi vx~16 = xi~16 – x1~16 vy~16 = yi~16 – y1~16 vz~16 = zi~16 – z1~16 !2. Calculate the unit direction vector u of the line L~16 = sqr((x2~16 – x1~16)^2 + (y2~16 – y1~16)^2 + (z2~16 – z1~16)^2) ux~16 = (x2~16 – x1~16)/L~16 uy~16 = (y2~16 – y1~16)/L~16 uz~16 = (z2~16 – z1~16)/L~16 !3. Find the projection of the point on the line L~16 = vx~16*ux~16 + vy~16*uy~16 + vz~16*uz~16
Chapter 18: Linear Algebra Applications - Closest Point on a Line 301 !4. Find the co-ordinates of the point x~16 = x1~16 + L~16*ux~16 y~16 = y1~16 + L~16*uy~16 z~16 = z1~16 + L~16*uz~16 return
A 2D variation is given below. "19 Closest Point on a Line – 2D": !Input: ! - a line defined by two points (x1~19, y1~19) - (x2~19, y2~19) ! - a test point (xi~19, yi~19) if getValues then x1~19 = get(1): y1~19 = get(1) x2~19 = get(1): y2~19 = get(1) xi~19 = get(1): yi~19 = get(1) getValues = 0 endif !Return: !
- the point on the line that is closest to the test point (x~19, y~19)
!Calculation: !1. Calculate vector v, starting at x1, y1, z1 and ending at xi, yi, zi vx~19 = xi~19 - x1~19 vy~19 = yi~19 - y1~19 !2. Calculate the unit direction vector u of the line L~19 = sqr((x2~19 - x1~19)^2 + (y2~19 - y1~19)^2) ux~19 = (x2~19 - x1~19)/L~19 uy~19 = (y2~19 - y1~19)/L~19 !3. Find the projection of the point on the line L~19 = vx~19*ux~19 + vy~19*uy~19 !4. Find the co-ordinates of the point x~19 = x1~19 + L~19*ux~19 y~19 = y1~19 + L~19*uy~19 return
18.9.2
Line or Line Segment?
This problem of calculating the closest distance to a line segment is slightly different to that of calculating the distance between a point and a line. We can calculate the distance between the point and the line easily enough, but we must also
302 Chapter 18: Linear Algebra Applications - Closest Point on a Line Segment check that the closest point on the line lies between the end points of the line segment. If it does not, then the closest point on a line segment is in fact one of the end points. The simplest example I can think of is a line segment between the points (0, 0, 0) and (1, 0, 0) and a point at (101, 0, 0). Obviously, while the point and the line segment both lie on the x-axis, there is a distance of 100 units between the end of the line segment and the point.
18.10
Closest Point on a Line Segment
In the preceding section we saw how to calculate the point on a line closest to a given point in space. Having found this closest point, we must now test whether it lies between the end points of the line segment. To do this, we can use the same approach we used in the sub-routine we constructed in the section Does a Point Lie on a Line Segment? If the point lies between the end points of the line segment, our job is done. Otherwise, we must choose either the start or end point to be our closest point.
18.10.1 A GDL Sub-Routine A GDL script that can calculate the point on a line segment that is closest to a given point might look something like this. "35 Closest Point on a 3D Line Segment": !Input: !
- a line segment defined by end points
!
(x1~35, y1~35, z1~35)
! !
(x2~35, y2~35, z2~35) - a point (xi~35, yi~35, zi~35) if getValues then x1~35 = get(1): y1~35 = get(1): z1~35 = get(1) x2~35 = get(1): y2~35 = get(1): z2~35 = get(1) xi~35 = get(1): yi~35 = get(1): zi~35 = get(1) getValues = 0 endif
!Return: ! !
- the point (x~35, y~35, z~35) on the line segment that is closest to (xi~35, yi~35, zi~35)
!Calculation: !1. Find the closest point on the line from which the segment is taken x1~16 = x1~35: y1~16 = y1~35: z1~16 = z1~35 x2~16 = x2~35: y2~16 = y2~35: z2~16 = z2~35
Chapter 18: Linear Algebra Applications - Closest Point on a Line Segment 303 xi~16 = xi~35: yi~16 = yi~35: zi~16 = zi~35 gosub "16 Closest Point on a 3D Line" !2. Find the length of the line segment L~35 = sqr((x2~35 - x1~35)^2 + (y2~35 - y1~35)^2 + (z2~35 - z1~35)^2) !3. By default, the calculated point (x~16, y~16, z~16) is the closest x~35 = x~16: y~35 = y~16: z~35 = z~16 !4. If the point lies beyond the start point, use the start point instead if L~16 < 0 then x~35 = x1~16: y~35 = y1~16: z~35 = z1~16 endif !5. If the point lies beyond the end point, use the end point instead if L~16 > 0
and L~35 > L~16 then
x~35 = x2~16: y~35 = y2~16: z~35 = z2~16 endif return
A 2D variation is given below: "34 Closest Point on a 2D Line Segment": !Input: !
- a line segment defined by end points
!
(x1~34, y1~34)
! !
(x2~34, y2~34) - a point (xi~34, yi~34) if getValues then x1~34 = get(1): y1~34 = get(1) x2~34 = get(1): y2~34 = get(1) xi~34 = get(1): yi~34 = get(1) getValues = 0 endif
!Return: ! !
- the point (x~34, y~34) on the line segment that is closest to (xi~34, yi~34)
!Calculation: !1. Find the closest point on the line from which the segment is taken x1~19 = x1~34: y1~19 = y1~34 x2~19 = x2~34: y2~19 = y2~34 xi~19 = xi~34: yi~19 = yi~34
304 Chapter 18: Linear Algebra Applications - Closest Point on a Line Segment gosub "19 Closest Point on a 2D Line – 2D" !2. Find the length of the line segment L~34 = sqr((x2~34 - x1~34)^2 + (y2~34 - y1~34)^2) !3. By default, the returned point (x~19, y~19) is the closest x~34 = x~19: y~34 = y~19 !4. If the point lies beyond the start point, use the start point instead if L~19 < 0 then x~34 = x1~34: y~34 = y1~34 endif !5. If the point lies beyond the end point, use the end point instead if L~19 > 0 and L~19 > L~34 then x~34 = x2~19: y~34 = y2~19 endif return
Note that in this sub-routine, we called another sub-routine. We also directly used the variables set in the second subroutine. This is quite acceptable, provided that you use unique identifiers for each sub-routine. If you don’t use identifiers, there is a high risk that you will set values in one sub-routine that are used in a second. This could easily cause unexpected results. Using the Sub-Routine You can use the 35 Closest Point on a 3D Line Segment sub-routine either within the main script, or from another subroutine. The following script fragment draws a line between a given point (xi, yi, zi) (marked by a sphere), and the closest point on a line segment with end points (x1, y1, z1) and (x2, y2, z2). !Main Script pen 1 lin_ x1, y1, z1, x2, y2, z2 add xi, yi, zi sphere .01 del 1 put x1, y1, z1, x2, y2, z2, xi, yi, zi gosub “35 Closest Point on a 3D Line Segment” pen 2 lin_ xi, yi, zi,
Chapter 18: Linear Algebra Applications - Closest Point on a Polygon 305 x_L07, y_L07, z_L07 end
To see how this works, create a new object with parameters x1, y1, z1, x2, y2, z2, xi, yi, and zi. Type the required headers at the head of the 3D Script editing window, then copy the above script into the 3D Script editing window after the headers. Copy the required sub-routines into the 3D Script editing window after the ‘end’ statement of the main script. Click on the 3D View button to show the result. Try changing the parameter values to see how the closest point is always calculated correctly.
18.11
Closest Point on a Polygon
This natural extension of the problem is discussed in chapter 21: Polygon Operations.
18.12
Distance between a Point and a Line Segment
Given a line segment that starts at co-ordinates (x1, y1, z1) and ends at co-ordinates (x2, y2, z2), and a point P at co-ordinates (xP, yP, zP), the problem is to find the distance between the point and the line (figure 262). The solution to this problem is an extension to the preceding one. By subtracting the resulting vector from that joining the start point of the line with P, we obtain a vector normal to the line that joins the line to point P. The length of this vector is the perpendicular distance between the point and the line, and hence the distance of closest approach. A GDL script that can use this calculation might read as follows. “36 Distance between Point and Line Segment”: !Input: ! – a line defined by start point (x1, y1, z1) !
and end point (x2, y2, z2)
! - a point (xi, yi, zi) if getValues then x1~36 = get(1): y1~36 = get(1): z1~36 = get(1) x2~36 = get(1): y2~36 = get(1): z2~36 = get(1) x3~36 = get(1): y3~36 = get(1): z3~36 = get(1) getValues = 0
Figure 262 – The challenge here is to calculate the distance between a point and a line segment.
306 Chapter 18: Linear Algebra Applications - Distance between Two Lines endif !Return: !
- Distance D~36
!Calculation: !1. Calculate the projection of the point on the line. x1~35 = x1~36: y1~35 = y1~36: z1~35 = z1~36 x2~35 = x2~36: y2~35 = y2~36: z2~35 = z2~36 xi~35 = xi~36: yi~35 = yi~36: z1~35 = zi~36 gosub “35 Closest Point on a 3D Line Segment” !2. Calculate the distance between the point and the line. D~36 = sqr((x~35 – xi~36)^2 + (y~35 – yi~36)^2 + (z~35 – zi~36)^2) return
This sub-routine calls the re-usable sub-routine 35 Closest Point on a 3D Line Segment we Figure 263 – Two codesigned earlier. We could alternatively design a single sub-routine that returns both the planar non-parallel lines closest point and the distance. with normal vector n.
18.13
Distance between Two Lines
In 2D, any given two lines are either parallel, or else they must intersect at some point. In 3D, just as in 2D, parallel lines never intersect. Non-parallel lines, however, do not necessarily intersect. It turns out that two lines can only intersect if a plane exists that contains both lines. We call such lines co-planar. Imagine two non-parallel lines in a plane (figure 263). At some point in this plane, they will intersect. At that point, the distance between the two lines is zero. If one of the lines is moved to a new position in the plane, the two lines will still intersect. The minimum separation between the two lines is still zero, and the point at which this occurs is the intersection point. The only way we can separate the two lines is to move one of them out of the original plane. Let’s say we move one of the lines by a distance L in the direction of the plane’s unit normal vector nˆ . While the lines no longer intersect, it is easy to see that the distance of closest approach is now L (figure 264). We can also see that the point on the stationary line at which this occurs is the original intersection point.
Figure 264 – If one of the two lines is moved along the normal vector by a distance D, this is clearly the lines’ Note that every point on the line we moved is the same perpendicular distance from the separation at the point of closest approach. original plane.
Chapter 18: Linear Algebra Applications - Distance between Two Lines 307 Our challenge, given any two lines in 3-space, is three-fold: 1.
To find whether the two lines are parallel
2.
If the lines are parallel, find the distance between them
3.
If the lines are not parallel, find the distance of closest approach
Check for Parallel Lines First, we need to check if the two lines are parallel. We already have a re-useable GDL subroutine to do this, so we’ll go ahead and re-use it. Distance between Parallel Lines If the two lines are parallel, we can find the distance between them using the following Figure 265 – Find the method. Describe each line parametrically in the form:
x
x1
= y z
y z
+ s uˆ uˆ
1
1
x y = z
x y z
uˆ x
uˆ + t uˆ uˆ
2
2
2
y
z
x
y
z
distance between parallel lines.
In these equations the values (x1, y1, z1) and (x2, y2, z2) are the co-ordinates of the start points of the two lines, and the values (ux, uy, uz) are the co-ordinates of the unit direction vector of the lines. Construct a vector v joining the start points of the first and second lines.
v = v v x
y
z
x y z
2
2
2
x − y z
1
1
1
The distance D between the vectors is the length of the difference of this vector and its projection on the first line (figure 265). D =
v − ( v .uˆ )uˆ
308 Chapter 18: Linear Algebra Applications - Distance between Two Lines Distance between Non-Parallel Lines To find the distance between two non-parallel lines, let’s go back to our earlier discussion. If the two lines were the same plane, the unit normal of that plane would be perpendicular to both the direction vectors.
nˆ= uˆ × vˆ We will choose that our first line lies in the original plane, and the second line has been moved orthogonally away from the plane by a distance D. Every point on the second line is at the same height above the plane surface, since the line is parallel to the plane. Thus, if we can find a vector that joins a point in the original plane to a point in the second line, the distance between the second line and the original plane can be calculated by taking the nˆ -component of this vector (figure 266). One such vector is the vector that joins p1 to p2 . Thus the distance L between the second line and the original plane is L = ( p2 − p1 ) ⋅ nˆ
We can combine all of the above calculations into a GDL sub-routine that can be used to calculate not only the distance between two lines, but also (in the case of non-parallel lines) the points on the lines of closest approach. Note that this sub-routine is extended to include the points of closest approach in the next section. "37 Distance Between Two Lines": !Input:
- Line 1: x1~37, y1~37, z1~37, x2~37, y2~37, z2~37
!
- Line 2: x3~37, y3~37, z3~37, x4~37, y4~37, z4~37 if getValues then x1~37 = get(1): y1~37 = get(1): z1~37 = get(1) x2~37 = get(1): y2~37 = get(1): z2~37 = get(1) x3~37 = get(1): y3~37 = get(1): z3~37 = get(1) x4~37 = get(1): y4~37 = get(1): z4~37 = get(1) getValues = 0 endif
!Return: !
- Are the lines are parallel?: parallelLines~37
!
- Line separation: D~37
!Required Sub-routines: !
- "2 Check Parallel"
!Calculations:
Figure 266 – Calculate the distance between nonparallel lines.
Chapter 18: Linear Algebra Applications - Distance between Two Lines 309 !Direction vectors ux1~37 = x2~37 - x1~37 uy1~37 = y2~37 - y1~37 uz1~37 = z2~37 - z1~37 ux2~37 = x4~37 - x3~37 uy2~37 = y4~37 - y3~37 uz2~37 = z4~37 - z3~37 !Make the direction vectors unit length L1~37 = sqr(ux1~37^2 + uy1~37^2 + uz1~37^2) ux1~37 = ux1~37/L1~37 uy1~37 = uy1~37/L1~37 uz1~37 = uz1~37/L1~37 L2~37 = sqr(ux2~37^2 + uy2~37^2 + uz2~37^2) ux2~37 = ux2~37/L2~37 uy2~37 = uy2~37/L2~37 uz2~37 = uz2~37/L2~37 !Check if the lines are parallel ux~2 = ux1~37: uy~2 = uy1~37: uz~2 = uz1~37, vx~2 = ux2~37: vy~2 = uy2~37: vz~2 = uz2~37 gosub "2 Check Parallel" parallelLines~37 = parallelLines~2 !Construct a vector joining the start points vx~37 = x3~37 - x1~37 vy~37 = y3~37 - y1~37 vz~37 = z3~37 - z1~37 !Calculate the separation distance and points of closest approach if parallelLines~37 then dotProduct~37 = vx~37*ux2~37 + vy~37*uy2~37 + vz~37*uz2~37 closestX1~37 = x1~37 closestY1~37 = y1~37 closestZ1~37 = z1~37 closestX2~37 = x3~37 + dotProduct~37*ux2~37 closestY2~37 = y3~37 + dotProduct~37*uy2~37 closestZ2~37 = z3~37 + dotProduct~37*uz2~37 D~37 = sqr((closestX2~37 - closestX1~37)^2 + (closestY2~37 - closestY1~37)^2 + (closestZ2~37 - closestZ1~37)^2) else
310 Chapter 18: Linear Algebra Applications - Find the Points of Closest Approach nx~37 = uy1~37*uz2~37 - uz1~37*uy2~37 ny~37 = uz1~37*ux2~37 - ux1~37*uz2~37 nz~37 = ux1~37*uy2~37 - uy1~37*ux2~37 D~37 = vx~37*nx~37 + vy~37*ny~37 + vz~37*nz~37 endif return
18.14
Find the Points of Closest Approach
An extension to the problem of finding the distance between two lines is to find the points at which this distance of closest approach is achieved. For parallel lines this is not possible, as there are an infinite number of such vertex pairs. We calculated one such pair to find the line separation in the preceding algorithm. To find the points of closest approach for two non-parallel lines, we will first calculate the separation D, and move the second line back along the unit normal by this distance so that the two lines are co-planar. We can then calculate the intersection point of the co-planar lines, and hence find one of the points of closest approach. Our second line equation will then become
(
)
x = p1 − Dnˆ + svˆ To find the point of intersection, we must solve the following system of equations for the variables s and t (All the other variables ( p1 , u , p2 , D , nˆ , v ) are already known, and can be treated as constants).
p1 x + su x = ( p2 x − Dnx ) + tvx p1 y + su y = ( p2 y − Dn y ) + tv y p1 z + su z = ( p2 z − Dnz ) + tv z Re-arranging the equations we get
u x s = p2 x − p1 x − Dnx + tv x u y s = p2 y − p1 y − Dn y + tv y u z s = p2 z − p1 z − Dnz + tv z
Figure 267 – Given two lines in 3-space, find the points of closest approach.
Chapter 18: Linear Algebra Applications - Find the Points of Closest Approach 311 We could choose any two of these equations to find t . Taking the first two equations, we get
t (u y v x − u x v y= ) D ( u y nx − u x n y ) + u y ( p1 x − p2 x ) − u x ( p1 y − p2 y ) t=
D ( u y nx − u x n y ) + u y ( p1 x − p2 x ) − u x ( p1 y − p2 y ) (u y v x − u x v y )
If the denominator is zero, we should use either the first and third equations, or the second and third equations to find t . We can then substitute the value of t back into one of the system equations to find s , and then use both s and t to find the values of x1 , y1 , and z1 at which the lines intersect. The other point of closest approach can be calculated by moving the first point along the normal vector by D (figure 268). Figure 268 – The first point can be found by first moving Rather than create a separate sub-routine to calculate the points of closest approach, we will line L2 back along the add some code to sub-routine “37 Distance between Two Lines”. We’ll use a flag variable normal direction by the calculatePoints~37 to tell the sub-routine whether or not it should calculate these points. separation distance, then If the flag is set to 1, the points will be calculated. The flag will be re-set to zero as soon as the finding the intersection of this and line L1. The second calculation has been performed. point can then be found by Edit the sub-routine 37 Distance between Two Lines as shown below. moving the first point along "37 Distance Between Two Lines": the normal by the separation !Input: distance.
x= x1 + Lnˆ 2
!
- Line 1: x1~37, y1~37, z1~37, x2~37, y2~37, z2~37
!
- Line 2: x3~37, y3~37, z3~37, x4~37, y4~37, z4~37 if getValues then x1~37 = get(1): y1~37 = get(1): z1~37 = get(1) x2~37 = get(1): y2~37 = get(1): z2~37 = get(1) x3~37 = get(1): y3~37 = get(1): z3~37 = get(1) x4~37 = get(1): y4~37 = get(1): z4~37 = get(1) getValues = 0 endif
!Flags: !
- Return points of closest approach?: calculatePoints~37
!Return:
312 Chapter 18: Linear Algebra Applications - Find the Points of Closest Approach !
- Are the lines are parallel?: parallelLines~37
!
- Line separation: D~37
!Required Sub-routines: !
- "2 Check Parallel"
!Calculations: !Direction vectors ux1~37 = x2~37 - x1~37 uy1~37 = y2~37 - y1~37 uz1~37 = z2~37 - z1~37 ux2~37 = x4~37 - x3~37 uy2~37 = y4~37 - y3~37 uz2~37 = z4~37 - z3~37 !Make the direction vectors unit length L1~37 = sqr(ux1~37^2 + uy1~37^2 + uz1~37^2) ux1~37 = ux1~37/L1~37 uy1~37 = uy1~37/L1~37 uz1~37 = uz1~37/L1~37 L2~37 = sqr(ux2~37^2 + uy2~37^2 + uz2~37^2) ux2~37 = ux2~37/L2~37 uy2~37 = uy2~37/L2~37 uz2~37 = uz2~37/L2~37 !Check if the lines are parallel ux~2 = ux1~37: uy~2 = uy1~37: uz~2 = uz1~37, vx~2 = ux2~37: vy~2 = uy2~37: vz~2 = uz2~37 gosub "2 Check Parallel" parallelLines~37 = parallelLines~2 !Construct a vector joining the start points vx~37 = x3~37 - x1~37 vy~37 = y3~37 - y1~37 vz~37 = z3~37 - z1~37 !Calculate the separation distance and points of closest approach if parallelLines~37 then dotProduct~37 = vx~37*ux2~37 + vy~37*uy2~37 + vz~37*uz2~37 closestX1~37 = x1~37 closestY1~37 = y1~37 closestZ1~37 = z1~37
Chapter 18: Linear Algebra Applications - Find the Points of Closest Approach 313 closestX2~37 = x3~37 + dotProduct~37*ux2~37 closestY2~37 = y3~37 + dotProduct~37*uy2~37 closestZ2~37 = z3~37 + dotProduct~37*uz2~37 D~37 = sqr((closestX2~37 - closestX1~37)^2 + (closestY2~37 - closestY1~37)^2 + (closestZ2~37 - closestZ1~37)^2) else nx~37 = uy1~37*uz2~37 - uz1~37*uy2~37 ny~37 = uz1~37*ux2~37 - ux1~37*uz2~37 nz~37 = ux1~37*uy2~37 - uy1~37*ux2~37 D~37 = vx~37*nx~37 + vy~37*ny~37 + vz~37*nz~37 !Calculate Points of Closest Approach if calculatePoints~37 then !Drop the second line back by -D along n onto the first line x3~37 = x3~37 – D~37*nx~37 y3~37 = y3~37 – D~37*ny~37 z3~37 = z3~37 – D~37*nz~37 !Calculate the intersection point Dxy~37 = uy1~37*ux2~37 – ux1~37*uy2~37 Dxz~37 = uz1~37*ux2~37 – ux1~37*uz2~37 Dyz~37 = uz1~37*uy2~37 – uy1~37*uz2~37 !Choose the best of the 3 denominators biggestD~37 = max(abs(Dxy~37), abs(Dxz~37), abs(Dyz~37)) if abs(biggestD~37 – abs(Dxy~37)) < tol then t~37 = (D~37*(uy1~37*nx~37 – ux1~37*ny~37) - uy1~37*vx~37 + ux1~37*vy~37)/Dxy~37 endif if abs(biggestD~37 – abs(Dxz~37)) < tol then t~37 = (D~37*(uz1~37*nx~37 – ux1~37*nz~37) - uz1~37*vx~37 + ux1~37*vz~37)/Dxz~37 endif if abs(biggestD~37 – abs(Dyz~37)) < tol then t~37 = (D~37*(uz1~37*ny~37 – uy1~37*nz~37) - uz1~37*vy~37 + uy1~37*vz~37)/Dyz~37 endif !One of the closest points is the intersection point closestX1~37 = x1~37 + t~37*ux1~37 closestY1~37 = y1~37 + t~37*uy1~37 closestZ1~37 = z1~37 + t~37*uz1~37 !The other point is the first, pushed by D along n closestX2~37 = closestX1~37 + D~37*nx~37
314 Chapter 18: Linear Algebra Applications - Distance between a Point and a Plane closestY2~37 = closestY1~37 + D~37*ny~37 closestZ2~37 = closestZ1~37 + D~37*nz~37 !Re-set the flag calculatePoints~37 = 0 endif endif return
Lines or Line Segments? If you are working with line segments, you should also check that the points of closest approach actually fall within the bounds of the line segments. Imagine two non-parallel line segments in the same plane. If they were lines, they would intersect, so their separation would be zero. But let’s say also that the line segments don’t actually extend to the intersection point. How do we go about finding the points of closest approach? In our sub-routines for calculating the intersection point, we first find values for s and t. These values are then substituted back into our line equations to get the intersection co-ordinates. Now the values s and t have a geometrical meaning too. These are the distances of the intersection point measured from the start points of the lines, along the directions of u and v. In other words, if the value of s is less than zero, the closest point on the first line segment to the actual point of closest approach is the start point. Similarly, if s is greater than the line length, we should choose the end point of the first line segment. The same argument may be applied to the second line segment. A value of t less than zero or greater than the second line length means that the point of closest approach lies beyond either the start or the end of the line segment, and so should be substituted by the start point or end point.
18.15
Distance between a Point and a Plane
Construct a vector v between points p and q . The projection of this vector onto the planes unit normal vector nˆ is the distance D between the point and the plane.
18.16
Closest Point on a Plane
An extension of this problem above is to find the point on the plane closest to point q . This point is found by starting at point q and moving by a distance -D along nˆ .
Chapter 18: Linear Algebra Applications - Intersection of a Line and a Plane 315
18.17
Intersection of a Line and a Plane
Given a point on a plane and the plane’s unit normal vector, and a line, we want to calculate the point at which the line penetrates the plane. As usual, the line is defined by a position vector q and a direction vector uˆ , while the plane is defined by a given point on the plane p and a unit normal vector nˆ . Unless the line is parallel with the plane we can calculate the point of penetration by the following method. Construct a new vector v between points p and q . At the point of penetration, we know that the resultant vector
0 for v + tu has no nˆ component – otherwise it would not lie in the plane. So if we solve the equation ( v + tu ) ⋅ nˆ = t, we can find the penetration point. In component form our equation is:
vx nx + tu x nx + v y n y + tu y n y + vz nz + tu z nz = 0 Solving for t we get
t= −
( v x nx + v y n y + v z nz )
(u n x
x
+ u y n y + u z nz )
.
If the line and plane are parallel, the denominator will be zero, otherwise we can calculate t directly and substitute back into our line equation x= q + tu to find the point of penetration. A GDL sub-routine might look something like this. “42 Point where a Line Penetrates a Plane”: !Given a line defined by a point q and direction vector u !and a plane defined by a point p and unit normal vector n !Calculate the point at which the line penetrates the plane D~42 = ux~42*nx~42 + uy~42*ny~42 + uz~42*nz~42 if abs(D~42) < tol then !Parallel Line else vx~42 = qx~42 – px~42 vy~42 = qy~42 – py~42 vz~42 = qz~42 – pz~42 t~42 = -(vx~42*nx~42 + vy~42*ny~42 + vz~42*nz~42)/D~42 x~42 = qx~42 + t~42*ux~42 y~42 = qy~42 + t~42*uy~42 z~42 = qz~42 + t~42*uz~42 endif return
19 Matrices and Transformations The study of matrices is a whole other field of mathematics. We will only lightly scratch its surface here. We will look at matrices merely as tools to perform transformations. Matrices are great tools for performing transformations. You can perform multiple transformations in a single step. In some cases you will want to apply the transformations using the xform command. In other cases, you will want to calculate the result of the transformations without actually performing them. In this chapter we’ll look at how unit transformation matrices can be constructed, how these operate on position vectors, and how multiple elementary transformation matrices can be combined to form a single matrix that performs a bunch of transformations in one shot. Before reading this chapter, you should understand some basic linear algebra. In particular, you should understand the concepts of position vector, direction vector and dot product.
19.1 Elementary Transformation Matrices A matrix is a set of numbers or expressions held in a rectangular array. The matrices that we will use in this chapter are 3 x 3 matrices, with three rows and three columns. The term elementary is given to transformations that perform a single operation. We will consider four elementary transformations – scale, rotation about the origin, change of axes and translation. The first three can be performed using transformation matrices. You can think of a transformation matrix as 3 vectors that represent our original (x, y, z) axes relative to the transformed (x, y, z) axes. Each axis is expressed as a direction vector. The three rows of the matrix hold the direction vectors of our original x, y and z axes. When we multiply a position vector by a transformation matrix, we are in fact calculating the dot product of the transformed position vector onto each axis direction vector. Effectively, we are answering the question, “What are the components of the transformed position vector when projected onto the original axes?” I’ll explain how this works when we consider a rotation about the origin. When we think of transformation matrices in this way, we can quite easily see how to construct a given elementary transformation matrix.
19.1.1
Rotation about the Origin
Let’s say we have to find the co-ordinates of a point (a position vector) (figure 269) rotated about the z-axis.
Chapter 19: Matrices and Transformations - Elementary Transformation Matrices 317 Obviously the z-component of the point will not be affected at all. That’s quite handy, because now we can draw our problem on the back of an envelope. The problem becomes that of calculating the transformed x and y components. To rotate the point through an angle θ, we’ll simply rotate our envelope. Unfortunately, our axis system has also rotated (figure 270). No problem – we’ll redraw the original axis system (figure 271). Figure 269 – A position vector u
We know the position vectors of our rotated points in the rotated axis system, but in the x-y co-ordinate system. not in the original axis system. Let’s draw some dotted lines to indicate the projection of the point onto the original axes. Our problem has changed to calculating the projection (dot product) of the rotated position vector onto the original axis direction vectors. If you turn the envelope back to its original orientation (figure 272), you can see that the direction vector of the original x-axis relative to the rotated axis system is (cos, sin, 0). Similarly, the direction vector of the original y-axis relative to the rotated axis system is (sin, cos, 0). Since the z-axis is unchanged, its direction vector remains (0, 0, 1). We can now construct our transformation matrix.
( cos θ − sin θ 0 ) ( sin θ cos θ 0 ) = Rz ( θ ) = ( 0 0 1)
cos θ sin θ 0
− sin θ cos θ 0
Figure 270 – On rotating our envelope, the point rotates as do the azes.
0
1 0
Similarly, we can construct matrices for rotations about the x and y axes.
x = Rx ( θ ) y z
0 1 0 cos θ 0 sin θ
x cos θ 0 R y (θ ) y = z − sin θ
x y − sin θ cos θ z 0
sin θ
x y 1 0 0 cos θ z 0
Figure 271 – Re-draw the original axes.
318 Chapter 19: Matrices and Transformations - Matrix Multiplication The problem of calculating the co-ordinates of a point rotated by an angle θ is identical to finding the projection of the position vector onto an axis system rotated by an angle -θ.
19.1.2
Scale
To scale (squash or stretch) an axis by a factor s, we use a vector that has the same direction as the original axis, but has length s. Our transformation matrix for a scale along the x-axis is then:
s 0 0 S x (s) = 0 1 0 0 0 1
Figure 272 – The original x-axis has new components (cosƟ, -sinƟ).
To reflect an axis, we can simply multiply its direction vector by -1. For example, setting the first row of the transformation matrix to (-1, 0, 0) is identical to a mulx command.
19.1.3
Change of Axes
Sometimes it’s handy to exchange the directions of two axes. You might want to draw a cylinder that runs along the xaxis, rather than the z-axis. Sure, you could do a 90 degree rotation, but then you might also want to swap the x and y axes for some reason, and things can quickly get difficult to follow. To exchange axes, simply define the new directions of the axis system. For example, if you want to define your new x axis to point along the current z-direction, set the first row in the array to (0, 0, 1). You will have to change the third row of the array as well, otherwise both the x and z axes would be pointing in the same direction. In practice, this just means swapping two rows of the matrix. For instance we could swap the x and z axes. The new matrix of axis vectors would then be (0, 0, 1), (0, 1, 0) and (1, 0, 0).
0 0 1 x z 0 1 0 y = y 1 0 0 z x
19.2 Matrix Multiplication To multiply two matrices together, calculate the dot product of each row in the first matrix with each column in the
Chapter 19: Matrices and Transformations - Matrix Multiplication 319 second. The result will be a new matrix. The dot product of the ith row of the first matrix with the jth column of the second matrix will occupy the ith row and jth column of the product matrix. For example, let’s multiply two transformation matrices – a scale and a rotation.
a 0 0 cos θ 0 1 0 sin θ 0 0 1 0
− sin θ cos θ 0
a ⋅ cos θ 0 = sin θ 1 0
− a ⋅ sin θ
0
0
1
cos θ
0
0
The element a ⋅ cos θ in the first row and first column is the dot product of the first row ( a
0
0 ) and the first
cos θ column sin θ . 0 A more complex example below multiplies two rotation matrices. One of these rotates by Ѳ, the other by – Ѳ.
cos θ sin θ 0
− sin θ cos θ 0
0 cos θ
0 − sin θ 1 0
2 2 cos θ + sin θ = sin θ cos θ − cos θ ⋅ sin θ 0
sin θ
0
1
cos θ
0
0
cos θ ⋅ sin θ − sin θ cos θ
0
sin θ + cos θ
0
2
2
0
1
1 0 0 = 0 1 0 0 0 1 Let’s do one more example – this time we’ll multiply two identical rotation matrices.
320 Chapter 19: Matrices and Transformations - Combined Transformations
cos θ sin θ 0
− sin θ
0 cos θ
cos θ 0
cos θ − sin θ = 2 cos θ ⋅ sin θ 0 2
cos 2θ = sin 2θ 0
− sin θ
0 sin θ 1 0
cos θ 0
0
1 0
−2 cos θ ⋅ sin θ
0
− sin θ + cos θ
0
2
− sin 2θ cos 2θ 0
2
2
0
1
0
1 0
19.3 Combined Transformations It turns out that we can multiply elementary transformation matrices together to create a single matrix that applies all of the elementary transformations in one shot.
T v = T1T2T3 ...Tn v In other words we can multiply all the matrices together before we apply them to our vector. This means that we can create a kind of ‘super transformation matrix’ that applies a bunch of elementary transformation matrices simultaneously.
19.3.1
Example
For example, imagine that we want to rotate by 90° about the z axis, then rotate by 90° about the the new x axis. Clearly this should have the same result as swapping the directions of the axis so that the new x axis points along the original y axis, the new y axis points along the original z axis and the new z axis points along the original x axis. Our first elementary transformation is a rotation by 90° about the z axis.
cos ( 90 ) − sin ( 90 ) 0 0 −1 0 sin ( 90 ) cos ( 90 ) 0 = 1 0 0 0 0 1 0 0 1 The second elementary transformation is a rotation through 90° about the new x axis.
Chapter 19: Matrices and Transformations - Translations 321
0 0 1 1 0 0 0 cos ( 90 ) − sin ( 90 ) = 0 0 −1 0 sin ( 90 ) cos ( 90 ) 0 1 0 Multiplying the two matrices together we get our combined transformation matrix.
0 −1 0 1 0 0 0 0 1 1 0 0 0 0 −1 = 1 0 0 0 0 10 1 0 0 1 0 We recognize the result as an exchange of x, y and z axes as would be expected. While this is a rather trivial example, it serves to illustrate the principle. Our example also illustrates that order is important. If we applied the transformations in the wrong order, we would get a different result.
1 0 0 0 −1 0 0 −1 0 0 0 −1 1 0 0 = 0 0 −1 0 1 0 0 0 1 1 0 0
19.4 Translations Translation is the one transformation that cannot be implemented using transformation matrices. Instead we simply add the translation vector after performing the matrix multiplication.
19.5 The xform Command The xform command combines the transformation matrix and translation vector as: xform m11, m12, m13, t1, m11, m12, m13, t2, m11, m12, m13, t3
where the mij values are the elements of the transformation matrix, and the ti values the translation vector. For example, let’s set up a combined transformation that includes a rotation about the z-axis by 45°, followed by rotation about the new x-axis by 30 degrees. Finally we’ll translate the reference frame by 1 unit along the x-axis.
322 Chapter 19: Matrices and Transformations - The xform Command We’ll first calculate our transformation matrix. 0 0 cos 45 − cos 30 sin 45 sin 30 sin 45 cos 45 sin 45 0 1 − sin 45 cos 45 0 0 cos 30 sin 30 = sin 45 cos 30 cos 45 − sin 30 cos 45 0 0 1 0 − sin 30 cos 30 0 sin 30 cos 30
Now we can write the xform command. xform cos(45), -cos(30)*sin(45), sin(30)*sin(45), 1, sin(45), cos(30)*cos(45), -sin(30)*cos(45), 0, 0, sin(30), cos(30), 0
Create a new object and copy the following script into its 3D Script editing window. !Use a single transformation s30 = sin(30) c30 = cos(30) s45 = sin(45) c45 = cos(45) xform c45, -c30*s45, s30*s45, 1, s45, c30*c45, -s30*c45, 0, 0, s30, c30, 0 brick .6, .1, .2 del 1 !!Use basic transformations !
addx 1
!
rotz 45
!
rotx 30
!
brick .6, .1, .2
!
del 3
When you run the first part of the script (with the second part commented out as shown), the result will be identical to when you run the second part of the script. Three transformations have been carried out in a single command. I would recommend using xform in the following cases: •
To swap x, y and z axes. It is easier to follow the xform command than rotating and mirroring the axes.
•
When a single transformation is demonstrably more efficient than using several basic transformations.
It’s not good practice to over-use xform for arbitrary rotations, as it can result in an unreadable script. When you do use xform, be sure to add a comment that clearly indicates why it has been used and what elementary transformations it is
Chapter 19: Matrices and Transformations - Calculating Natural Co-ordinates 323 intended to replace.
19.6 Calculating Natural Co-ordinates Every object has a natural co-ordinate system. Its origin is at the placement point, and its rotation is as placed by the user. Particularly when you are creating freeform bodies, it can be useful to calculate the natural co-ordinates of a point that has been transformed by several elementary transformation matrices. In this case, multiply the local position vectors by the combined matrix to obtain numeric values for the co-ordinates.
20 Freeform 3D Modeling Many of the structures and forms you model using GDL can be constructed using the regular 3D building blocks described in the preceding chapters. However, there are whole families of objects that require more precise control. GDL primitives extend the relevance of GDL beyond pre-defined forms to allow the user to model any 3D surface. The primitives are built up systematically. First a set of vertices is defined in 3-space. Pairs of vertices are joined to form a net of edges. Each mesh of the net defines a polygon. Finally, the polygons are joined to form a solid body. This system allows you to construct any conceivable 3D form. Direct control over each vertex, edge and polygon provides total flexibility of design, along with exact Figure 273 – Freeform bodies such as control over the polygon count. this plant can’t be constructed from Freeform modeling can be somewhat mind-bending when you first start using it. However, the modeling freedom that it provides is well worth the effort, and as with everything, it gets easier with practice. So if you struggle at first, don’t be discouraged. Try relatively simple models until you become confident, then have a go at more complex modeling. In this chapter, we’ll start by defining the basic elements used in freeform modeling. We’ll use them to construct a picture polygon, a simple 3D form, and a ‘freeform’ solid. We’ll also see how they can be used to map textures onto bodies.
20.1 GDL Primitives In this section, we will look at the elements used to construct freeform bodies, and briefly consider how they are used in practice. This is a theoretical section. We’ll apply the theory in the following sections. GDL primitives are used to construct a body. First a set of points or vertices is defined in space. Pairs of vertices are then joined to form edges. Next the edges are joined to define polygons. Finally, a closed net of polygons is grouped to form a solid body.
using regular GDL elements. They require the control over individual vertices, edges and polygons offered only by the GDL ‘primitives’.
Chapter 20: Freeform 3D Modeling - GDL Primitives 325
Figure 274 – A sphere showing how it is constructed by defining a set of vertices, then by joining the vertices to define a net of edges, and finally using these edges to form a surface of polygons.
GDL primitives can also be used to map textures to existing bodies. For standard 3D elements such as prisms or cylinders, primitives provide limited control over how the texture is mapped onto the body’s surface. Much greater control is available when constructing bodies from primitives.
20.1.1
Base
The GDL interpreter refers to each vertex, edge or polygon by an index. The index of an edge is set by the order in which the edge was defined. The first edge that occurs in a GDL script is referred to by the index 1, the second edge has index 2, and so on. The same is true of vertices and polygons. The first vertex to be constructed has index 1, the second 2, etc. Similarly, the 53rd polygon to be constructed is referred to by index 53. All the regular GDL bodies (prisms, spheres, cylinders, tubes and the like), are built from primitive components. As a result, when you come to construct a body using primitives, a large number of vertices, edges and polygons may have already been defined. Thus the vertex, edge and polygon indices are not necessarily set to zero. It is essential that you use the base command to re-set the indices to zero whenever you begin to define a new body. This ensures that, when you refer to edge 1, you are referring to the first primitive edge element that you defined, not to an unrelated edge that was defined, say in the process of constructing a cylinder earlier in the 3D Script. To re-set the indices for vertices, edges and polygons, use the base command. In a GDL script, the base command occupies a complete line of script as shown below: !Syntax to Re-set Indices base
20.1.2
Define a Vertex
To define a vertex, use either the GDL primitive vert or teve command. !Syntax to Define a Vertex
326 Chapter 20: Freeform 3D Modeling - GDL Primitives vert x_coordinate, y_coordinate, z_coordinate !Syntax to Define a Texture-mapped Vertex teve x_coordinate, y_coordinate, z_coordinate, texture_a_coordinate, texture_b_coordinate
In the teve element, the texture co-ordinates anchor the vertex onto a texture picture. By definition the texture picture is 1 unit wide and 1 unit tall. The texture a and b co-ordinates take a value between 0 and 1. When three or more vertices are linked to form a complete polygon, the texture picture is stretched onto the polygon using those anchors. (Actually, the texture co-ordinates may take any value – the texture picture may be repeatedly tiled onto the surface). We’ll look at texture mapping in more detail later. The vertices you define are not visible in 3D. Only edges and polygons can be visible. However, you must define the vertices before you can define the edges, as the start and end points of the edges are the indices of previously defined vertices. Because the vertices are invisible, it is difficult to check them before moving on to the edges. To check that the vertices are in the correct locations, substitute small spheres. When you’re sure that all the vertices are in the right position, change the spheres back into vertices and move on to the next stage – defining edges.
20.1.3
Define an Edge
To define an edge, use the GDL primitive edge command. edge vertex_index_1, vertex_index_2, associated_polygon_1, associated_polygon_2, edge_status
The first two arguments are the vertex indices of the start point and the end point of the edge. Because each edge starts and ends on a previously defined vertex, you need only give the index of that vertex. This demands a methodical approach, especially if you are working with a large number of vertices. It is not uncommon for a freeform model to contain thousands of vertices. Provided these are created in a systematic way, it is possible to define the edges without too much difficulty. You must also be aware of the edge direction, as we’ll see later. The next two arguments are the indices of the polygons that border on the edge. If the edge is part of a solid body, there will always be two polygons sharing the edge. For open surfaces, the outer edges will only be used by one polygon, so the second polygon index should be set to zero. I prefer to define edges in groups. For example, I would define all the ‘horizontal’ or ‘latitude’ edges first. Next I would define the ‘vertical’ or ‘longitude’ edges. Finally, for a triangular net, I would define the ‘diagonal’ edges. Associating polygons to an edge before the polygons have been created requires a certain degree of fore-knowledge. There are two techniques that can help. •
The simplest approach is to set both polygon indices to -1. If you do so, the GDL interpreter will automatically search the polygon list to assign the correct indices.
•
Later we will see how we can use arrays to store the vertices, edges and polygons prior to placing them. This allows us to associate polygons with edges more easily.
Chapter 20: Freeform 3D Modeling - GDL Primitives 327 The final argument (edge_status) is the edge status. This value controls the visibility of the edge. The three available options are: 0 = always visible 1 = invisible edge 2 = edge of a curved surface (i.e. visible only for views in which it contributes to the body’s outline)
20.1.4
Define a Vector
Vectors are used to define the normal for the polygons. I tend not to define them, as they are calculated automatically with no effect on performance. vect x_component, y_component, z_component
20.1.5
Define a Polygon
We’ve outlined how to define vertices and edges to form a net. Now we need to form polygons from ordered groups of edges. Use the GDL primitive pgon command to define polygons. This command allows you to create polygons of any number of edges. !Syntax to Define a Polygon pgon number_of_edges, normal_vector, status, first_edge_index, second_edge_index, … last_edge_index
The first argument is the number of edges defining the polygon. While the polygon may contain more than three edges, a triangular polygon is most efficient. Polygons with 4 or more edges are triangulated internally anyway, which of course requires more processing time. The second argument is the index of the polygon’s outward normal vector. This vector must have been previously defined. Alternatively, you can set this value to zero if you want the GDL interpreter to calculate the outward normal. I do this all the time. There is no tangible gain in performance by explicitly defining the normal vector. Status Value The polygon’s status value determines a number of characteristics. Add the appropriate values from the table below to get the correct status value for a polygon. If you’re not sure of the status, just set it to -1 and the GDL interpreter will make the decision. 1
Invisible polygon
2
Polygon is part of a curved surface
328 Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs 4
Not used in ArchiCAD 14
8
Not used in ArchiCAD 14
16
Polygon may be concave
32
Polygon contains hole
64
Convex hole – if 32 is in force
If you are defining the polygon shape directly, you will know whether or not the polygon is concave. Triangular polygons, for example, can never be concave. If the polygon is the result of user input, or of a random process, then it is safe to assume that at some stage it will become concave, so add the 16 to be safe. Edge Indices The edge indices must be defined in order. Edge direction is important – if the polygon requires the edge to point in the opposite direction to which it was initially defined, add a minus sign before the index of that edge. Non-Planar Polygons All the vertices in a polygon must lie in the same plane, otherwise the body will fail to generate. This means that you must define the vertices for each polygon in such a way that you are certain they are co-planar. Obviously the vertices of a triangular polygon are always co-planar. So by using triangular polygons exclusively we can be certain to meet this criterion.
20.1.6
Define a Body
The final step is to group the vertices, edges and polygons into a complete body. !Syntax to Define a Body body -1
20.2 Cardboard Cut-Outs Organic objects such as trees, people and animals, add life to sections, elevations and 3D views (figure 275), but do not form part of the building structure. If they are modeled as 3D forms, such objects tend to have a high polygon count, making the 3D model take longer to build. One way to avoid the slow-down, while still producing attractive 3D models, is to use the cardboard cut-out technique. Figure 275 - Organic objects such as trees tend to have a high polygon count.
Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs 329 This technique takes a .tif image, and pastes it onto a 3D polygon of the same shape as the image. The result is reminiscent of a cardboard cut-out figure you might see in a store. This technique works only for still views. Cardboard cut-outs are not well suited to fly-throughs or virtual reality movies.
20.2.1
Create a ‘Cut-Out’ Object
To create a cardboard cut-out, use the pipg command. The word pipg stands for ‘picture polygon’. To create a picture polygon, you need two things – the shape of the polygon, and the picture. You can get the picture from a variety of sources – scanned images, digital cameras, etc. For best results, remove the background using the techniques described in the section on Pictures with Transparency. Note that the image transparency will be applied in photorendering, not in the 3D window. The polygon shape is defined by a set of points or vertices, Figure 276 – Use the ArchiCAD Fill tool to trace around the connected by edges. image. Place the picture into the ArchiCAD plan view window. If you want to control the dimensions of the cut-out object, re-size the image so that its defining dimension (in this case the height of the tree) is 1m. Trace around the image using a fill polygon (figure 276). Provided the picture has transparent areas, it is not necessary to be perfectly accurate when defining the fill polygon, so long as the polygon contains the whole picture. The polygon may contain holes. Drag the fill polygon into the object’s 2D Script to get an automatic listing of all the polygon edges (figure 277). The 2D Script will include a bunch of lines starting with the word HOTSPOT2 (figure 278). These lines can be deleted. Also, delete the first lines including the line starting with the word POLY2B {2}, and the line following.
Figure 277 – Drag the fill polygon into the 2D Scrupt editing window.
330 Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs You will be left with a lot of numbers separated by commas. Copy these numbers to the 3D Script, and type the word put before the first number (figure 279). Now we have stored the x, y co-ordinates of the vertices on the stack for use by the pipg element. To complete the 3D Script, we must add code that collects the data from the stack and uses it to create a cardboard cut-out object. The cut-out object is to stand upright, so we’ll also need to rotate it into an upright position using the xform command. ! Rotate the cut-out object into a vertical position
Figure 278 – A 2D script will be written, in which the polygon edges will be listed one by one.
xform 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0
We’ll need access to the vertex co-ordinates when we create the main polygon and sub-polygons, so the next bit of code retrieves the data from the stack and stores it in array variables. !Vertex Co-ordinates dim n[], x[][], y[][] nPg = 1
!Number of polygons
n[nPg] = 1!Number of vertices in each polygon x[nPg][1] = get(1)
!Vertex x-ordinates
y[nPg][1] = get(1)
!Vertex y-ordinates
while nsp do xi = get(1) yi = get(1) if (abs(xi - x[nPg][1]) < tol and abs(yi y[nPg][1]) < tol) then if nsp then nPg = nPg + 1 x[nPg][1] = 0 y[nPg][1] = 0 n[nPg] = 0 endif
Figure 279 – A little editing will be required to convert this to a 3D script.
Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs 331 else n[nPg] = n[nPg] + 1 x[nPg][n[nPg]] = xi y[nPg][n[nPg]] = yi endif endwhile
Now we will use the vert command to create the vertices as points in space. Before creating the first vertex, the GDL script must first re-set the primitive element counter using the base command. ! Vertices for each sub-polygon base for i = 1 to nPg for j = 1 to n[i] vert x[i][j], 0, y[i][j] next j next i
We’ll now join the vertices to form the edges that will be used to define the main polygon and any holes that it contains. !Join the vertices to form edges kTot = 0 k = 0 for i = 1 to nPg k = 0 for j = 1 to n[i] k = k + 1: nxt = k % n[i] + 1 edge kTot + k, kTot + nxt, -1, -1, 0 next j kTot = kTot + k next i
Having created all the edges, join them to form the main polygon plus any holes. !Join the edges to form the polygon kTot = 0 k = 0 for i = 1 to nPg k = 0 for j = 1 to n[i] k = k + 1
332 Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs put kTot + k next j kTot = kTot + k if i < nPg then put 0 next i
Finally the picture polygon is created. !Create the Picture Polygon pipg imageFile, a, zzyzx, 1, nsp, 0, 32 + 64, get(nsp)
The picture will not yet map onto the polygon. By default, the texture mapping is applied in the x-y plane. We need it to shift to the x-z plane. To do this, we must define three vertices that define the origin and the x, y and z directions. Then we define a new co-ordinate system that maps the three vertices to the x, z and y directions. For more details, see the section on Advanced Texture Mapping, but for now, just copy the code into your object. !Texture Mapping base vert 0, 0, 0 vert .001, 0, 0 vert 0, .001, 0 vert 0, 0, .001 coor 1, 1, 2, 4, 3 body -1
Note that the picture does not need to have transparency for the pipg to work. However, for complex shapes such as trees, it is easier to use Photoshop to produce a picture with transparent areas than it is to create an accurate fill polygon. You can make the cardboard cut-out automatically turn to face the camera in perspective views. This can be useful if you are doing some still shots, but for a fly-through it gives an eerie impression as people and trees turn to watch you as you fly by.
20.2.2
A Macro to Create Cut-Outs
If you’re going to make a bunch of cardboard cutouts, you might want to use a macro to create them. The individual objects put the polygon points on the stack, then call the macro which creates the cut-out object. Create a macro object called cadi_CardboardCutout with the following parameters.
Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs 333 Variable
Type
Name
Default Value
imageFile
string
Image File
Cutout Image
A
length
Image Width
300mm
ZZYZX
length
Image Height
500mm
faceCamera
bool
Turn to Face the Camera
Off
Type the following GDL script into the macro’s 3D Script editing window. !Given an image file and a set of polygon vertices (on the stack), !Create a cardboard cut-out that can automatically turn to face the camera in perspective views tol = .00001 !Position at the center addx -a/2 !Hotspots on the bounding box hotspot 0, 0, 0 hotspot a, 0, 0 hotspot a,0,zzyzx hotspot 0,0,zzyzx !End if there is no polygon data if nsp = 0 then end !Rotate to face the camera mulx 1 - 2*symb_mirrored dx = glob_eyepos_x - symb_pos_x dy = glob_eyepos_y - symb_pos_y if abs(dx) < tol then if dy > 0 then cameraAngle = 90 else cameraAngle = 270 endif else cameraAngle = atn(dy/dx) if dx < 0 then cameraAngle = 180 + cameraAngle endif mulx 1 - 2*symb_mirrored if turnToCamera then rotz 90 - symb_rotangle + cameraAngle
334 Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs !Vertex Co-ordinates & Status Values (not used) dim n[], x[][], y[][], s[][] nPg = 1 n[nPg] = 1 x[nPg][1] = get(1) y[nPg][1] = get(1) s[nPg][1] = get(1) while nsp do xi = get(1) yi = get(1) si = get(1) if (abs(xi - x[nPg][1]) < tol and abs(yi - y[nPg][1]) < tol) then if nsp then nPg = nPg + 1 x[nPg][1] = 0 y[nPg][1] = 0 s[nPg][1] = 0 n[nPg] = 0 endif else n[nPg] = n[nPg] + 1 x[nPg][n[nPg]] = xi y[nPg][n[nPg]] = yi s[nPg][n[nPg]] = si endif endwhile !Find the bounding rectangle x0 = x[1][1] z0 = z[1][1] x1 = x[1][1] z1 = z[1][1] for j = 1 to n[1] x0 = min(x0, x[1][j]) z0 = min(z0, z[1][j]) x1 = max(x1, x[1][j]) z1 = max(z1, z[1][j])
Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs 335 next j !Transform all the points to fit the dimensions A * ZZYZX for i = 1 to nPg for j = 1 to n[i] x[i][j] = (x[i][j] - x0)*(a/(x1 - x0)) z[i][j] = (z[i][j] - z0)*(zzyzx/(z1 - z0)) next j next i !Vertices for each sub-polygon base for i = 1 to nPg for j = 1 to n[i] vert x[i][j], 0, y[i][j] next j next i !Join vertices to form edges kTot = 0 k = 0 for i = 1 to nPg k = 0 for j = 1 to n[i] k = k + 1 nxt = k % n[i] + 1 edge kTot + k, kTot + nxt, -1, -1, 0 next j kTot = kTot + k next i !Join edges to form the polygon kTot = 0 k = 0 for i = 1 to nPg k = 0 for j = 1 to n[i] k = k + 1 put kTot + k next j
336 Chapter 20: Freeform 3D Modeling - Cardboard Cut-Outs kTot = kTot + k if i < nPg then put 0 next i !Picture Polygon pipg imageFile, a, zzyzx, 1, nsp, 0, 32 + 64, get(nsp) !Texture Mapping base vert 0, 0, 0 vert 0.001, 0, 0 vert 0, 0.001, 0 vert 0, 0, 0.001 coor 1, 1, 2, 4, 3 body -1
Note that this macro will not work as a stand-alone object, since it relies on the calling object to place the vertex coordinates onto the stack.
20.2.3
Using the Cut-Out Macro
To use the macro, first put the x, y co-ordinates of all the polygon vertices on the stack, then call the macro, defining the image file name, width and height. For example, the following script uses the macro to create a cardboard cut-out tree. put 0.7743111562291, -0.0005357583132464, 1, 0.8952726108251, 1.804978689224E-005, 1, 0.8870233115144, 0.014041858615, 1, 0.881248801997, 0.03384017696057, 1, ... 0.3861005370584, 0.4919481727316, 1, 0.406183056999, 0.4904033635054, -1 call "cadi_CardboardCutout" parameters a = a, b = b, zzyzx = zzyzx, imageDims = imageDims, imageFile = "Sycamore"
Chapter 20: Freeform 3D Modeling - Basic Texture Mapping 337
20.2.4
Improving the Macro
As it stands, the macro is good but could be improved. The macro’s main shortcoming is that it can only deal with fills comprised entirely of straight edges. In the section of script commented !Vertex Co-ordinates & Status Values (not used), the status value si is totally ignored. A better approach would be to interpret each status value (e.g. 1 = a straight edge, 900 = an arc center, 1001 = a tangential curve, etc.) so that you are no longer restricted to using straight edges when drawing the fill polygon. Also, the status value -1 indicates the end of a sub-polygon (main polygon or hole), so this would take the guess-work out of where the sub-polygons start.
20.3 Basic Texture Mapping Regular 3D elements such as spheres, cylinders, blocks and prisms automatically map the texture of whatever material is applied to them. GDL provides two controls for mapping a texture onto a regular 3D element. You can change the wrapping mode and the texture’s axis system. 21 The most common reason for changing the texture mapping is to change the direction of the default texture mapping. A typical example of this is wood grain. When you use a 3D element to represent a timber member, you know which direction the wood grain should run. Depending on which 3D element you used to model it, and how it was used, the default texture Figure 280 – Bring up the 3D Window Settings dialog. mapping may well choose a different direction. Try it Yourself: Apply a Texture to a Block To see what I mean, we’ll use the block element to model a timber post. Create a new object and add a material-type parameter with variable timberMat. Add three length-type parameters with variables postWidth, postThick and postHeight. Choose sensible default values for the length-type parameters, and choose a material with a timber grain for timberMat. Figure 281 – Choose the OpenGL Copy the following GDL script into its 3D Script editing window. material timberMat block postWidth, postThick, zzyzx
21
Oddly, tubes do not map textures successfully.
engine and Shading mode.
338 Chapter 20: Freeform 3D Modeling - Basic Texture Mapping Click on the 3D View button to run the script. Choose View > 3D View Mode > 3D Window Settings (figure 280) to bring up the 3D Window Settings dialog. From the dialog, choose the OpenGL engine with the Shading mode (figure 281). Zoom in to examine the woodgrain texture that is mapped onto your post. You’ll see that the texture is running horizontally rather than vertically up the post (figure 282). I’m no engineer, but I don’t think this is an ideal look for a post. We need to re-define the x, y, z axis system so that the texture is mapped vertically rather than horizontally. Edit the 3D Script so that it reads as follows.
Figure 282 – The wood grain runs across the post – not a good look.
material timberMat block postWidth, postThick, zzyzx base vert 0, 0, 0 vert 1, 0, 0 vert 0, 1, 0 vert 0, 0, 1 coor 2 + 1024, 1, 4, 2, 3 body -1
Run the 3D Script again, and zoom in to examine the texture mapping. This time the grain is running up the post – a much more satisfactory result (figure 283)! Figure 283 – By specifying the texture mapping co-ordinates, we can control First, we added the command base after the block element. This command re-sets the direction of the timber grain.
So how did we change the texture orientation?
the counter for the GDL primitives. Next, we defined a new co-ordinate system for the texture. To define a texture co-ordinate system, place a vertex at the origin (vert 0, 0, 0) and the tip of each axis (vert 1, 0, 0 etc.). Finally, use the coor command to identify which vertices define the origin and the x, y and z axes. coor texture_mapping_method, origin_vertex, x_axis_vertex, y_axis_vertex, z_axis_vertex
Chapter 20: Freeform 3D Modeling - Basic Texture Mapping 339 In the example, we specified that vertex 1 should be used for the origin, vertex 4 for the new x-axis, 2 for the new y-axis, and 3 for the new z-axis. The texture mapping method tells the GDL interpreter how you want to wrap the texture around the 3D element. For regular 3D elements, there are 5 texture mapping methods, plus a modifier that forces the material’s fill pattern to start at the local origin. 1
Planar mapping in the local x-y plane.
2
Map to a rectangular box (useful for prisms, blocks and bricks)
3
Cylindrical (not very useful except for cylinders, which map their textures automatically anyway) Figure 284 – Try fitting the post into
4
Spherical (only really useful except for use with spheres).
5
Cylindrical with the top/bottom surfaces mapped radially.
256
The fill always starts at the local origin.
the 3D window – it moves away from the center of the window (in this case to the top left corner).
1024 Quadratic texture projection (whatever that means). The 256 modifier links the fill and texture origins. Provided the texture and fill patterns are set to the same dimensions, this could be useful. However, there is no guarantee that this will be the case. This is the weakest part of the system, as it works well only for a limited range of 3D forms. Tubes, for example, simply don’t map textures well, although there is no reason why this should be the case. Finally use the body -1 statement to identify the end of the texture mapping definition.
20.3.1
Negative Vertex Indices
There is just one problem with our example. Try fitting the post into the 3D window. You can see that it sort of moves off to one side (figure 284). It almost looks as if there are some invisible 3D elements extending its bounding box. Figure 285 – Define the texture coordinates using vertices with negative In fact, it is the vertices we defined which are expanding the object’s bounding box. To avoid this, we need to inform the GDL interpreter that the vertices should not be used as part of the 3D model but only to define the texture co-ordinates. Add a minus sign before the vertex indices in the coor command.
index.
340 Chapter 20: Freeform 3D Modeling - 3D Modeling coor 2 + 1024, -1, -4, -2, -3
Edit the 3D Script, and click on the 3D View button to run the edited script. Now when you fit the post into the 3D window, it fits correctly (figure 285).
20.4 3D Modeling In the first section of this chapter I introduced the concept of primitive elements, and briefly outlined the general approach of using vertices, edges and polygons to build a solid body. We then used this approach to create a picture polygon. In this section, we’ll create a 3D body – a sphere. GDL already has a perfectly good sphere available for use, so this example is provided only to give an insight into what is going on without the math getting too complicated. It should not be used in practice. The structure of the sphere is based on a globe of the Earth. Lines of longitude and latitude run around it, forming a sort of grid in which the lines of longitude come together at the poles. To make scripting easier, we’ll refer to the latitudes as horizontals and the longitudes as verticals. That way, we can use the abbreviation ‘vert’ to mean ‘vertical’ and ‘horz’ for ‘horizontal’. We’ll also use ‘diag’ for diagonal. In the following sections we’ll look at how to construct elements that begin to introduce some freeform shapes into ArchiCAD. Each type of surface uses a similar approach. We will systematically define a set of vertices, edges and polygons, then combine them to form a solid body.
20.4.1
Vertices
Create a new object and add a length-type parameter with variable R of positive value. Open the 3D Script editing window. Our first task is to define the vertices on a sphere of radius R. We will start by placing a vertex at the ‘South Pole’ . !Vertices base !Vertex at the South Pole vert 0, 0, -R
We will now place vertices in rings around each ‘latitude’, where the ‘latitudes’ are separated by an incremental angle dPhi. For each latitude, a vertex will be placed where it intersects with a line of ‘longitude’ !Vertices at the intersection of each line of latitude and longitude nPhi = 18 nRho = 36 dPhi = 180/nPhi
Chapter 20: Freeform 3D Modeling - 3D Modeling 341 dRho = 360/nRho for phi = -90 + dPhi to 90 - dPhi /2 step dPhi for rho = 0 to 360 - dRho/2 step dRho vert R*cos(phi)*cos(rho), R*cos(phi)*sin(rho), R*sin(phi) next rho next phi
Finally we’ll place the vertex at the North Pole to complete the set. !Vertex at the North Pole vert 0, 0, R
If you want to ‘see’ the vertices, add a line resol 6 at the top of the script, then go Figure 286 – To ‘see’ the vertices, temporarily replace them with small through the script and replace every vert x, y, z with: spheres.
add x, y, z sphere .005 del 1
Click on the 3D View button to run the script. If it looks like this (figure 286), then you copied everything correctly. Change the adds back to verts, and delete the spheres and dels. Note that there are 2 polar vertices, and nPhi – 1 horizontal rings each containing nRho vertices.
20.4.2
Edges
Having created the vertices, we’ll now use them to define edges that will form a net. We want all our polygons to be triangular, so each vertex (except the poles) will have a horizontal edge, a vertical edge, and a diagonal edge starting from it. Let’s begin by creating all of the horizontal (latitudinal) edges. !Edges nHorz = nPhi - 1 nVert = nRho !Horizontals for i = 1 to nHorz firstVertex = 1 + (i - 1)*nVert for j = 1 to nVert nxt = j%nVert + 1
Figure 287 – The vertices are used to define a set of ‘latitudinal’ edges.
342 Chapter 20: Freeform 3D Modeling - 3D Modeling edge firstVertex + j, firstVertex + nxt, -1, -1, 0 next j next i
Now when you click on the 3D Full View button you’ll see we’ve created a set of horizontal edges forming lines of latitude (figure 287). Our next step is to create the vertical lines of longitude. We’ll include the edges that start at the South Pole vertex, and the edges that terminate at the North Pole vertex. !Verticals !Verticals starting at the South Pole for j = 1 to nVert edge 1, 1 + j, -1, -1, 0 next j !Regular verticals for i = 1 to nHorz - 1 firstVertex = 1 + (i - 1)*nVert
Figure 288 – The same vertices are used to define a set of ‘longitudes’.
for j = 1 to nVert edge firstVertex + j, firstVertex + nVert + j, -1, -1, 0 next j next i !Verticals that terminate at the North Pole firstVertex = 1 + (nHorz - 1)*nVert lastVertex = 1 + nHorz*nVert + 1 for j = 1 to nVert edge firstVertex + j, lastVertex, -1, -1, 0 next j
Figure 289 – Finally a set of diagonal
Check that the 3D Script is still good by clicking on the 3D View button (figure edges is defined. This breaks the 288). latitude – longitude grid into a set of Finally we’ll create a set of diagonal edges (figure 289).
triangles.
In the case of a sphere, these are not strictly necessary – we could use quadrilaterals since all of the quadrilateral polygons on the sphere’s surface are made up of co-planar edges. We’ll include the diagonals anyway, so that we can force all of the polygons to be triangular. !Diagonals for i = 1 to nHorz - 1
Chapter 20: Freeform 3D Modeling - 3D Modeling 343 firstVertex = 1 + (i - 1)*nVert for j = 1 to nVert nxt = j%nVert + 1 edge firstVertex + j, firstVertex + nVert + nxt, -1, -1, 0 next j next i
That completes all the edges required for the sphere. Now we’re ready to start defining polygons.
20.4.3
Polygons
Our first polygons will form the base of the sphere. The edges at the base are all pointing away from the South Pole. To form the ith polygon, we run up the i + 1th vertical (this starts at the pole and runs up to the first horizontal ring). We then run back along the ith horizontal, giving it a negative index, and finally back along the ith vertical. The index of the jth edge on the first horizontal ring is j. There are a total of nHorz*nVert horizontal edges, so the index of the jth vertical edge starting at the Figure 290 – Polygons have now been created for the two poles, and one South Pole is nHorz*nVert + j. polygon has been created for each
Wherever possible, I’ve pre-calculated the start index of each set of edges (eg. latitude-longitude grid. firstVert = nHorz*nVert for the first ring of verticals). !Polygons !Polygons around the South Pole firstVert = nHorz*nVert for j = 1 to nVert nxt = j%nVert + 1 pgon 3, 0, 0, -(firstVert + j), firstVert + nxt, -j next j
Now we’ll cautiously move to the polygons on the latitudes. Each latitude-longitude intersection (except the top ring) has two triangular polygons. We’ll add them one by one. Figure 291 – The second triangular polygon has now been created in each Add the following script to the 3D Script editing window. !Regular Polygons for i = 1 to nHorz - 1
latitude-longitude grid. This completes our set of polygons for the surface.
344 Chapter 20: Freeform 3D Modeling - 3D Modeling firstHorz = (i - 1)*nVert firstVert = nHorz*nVert + i*nVert firstDiag = nHorz*nVert + (nHorz + 1)*nVert + (i-1)*nVert for j = 1 to nVert nxt = j%nVert + 1 pgon 3, 0, 0, firstHorz + j, firstVert + nxt, -(firstDiag + j) next j next i
Click on the 3D View button to view the result (figure 290). Modify the script you just pasted as shown below to add the second polygon at each node (figure 291). !Regular Polygons for i = 1 to nHorz - 1 firstHorz = (i - 1)*nVert firstVert = nHorz*nVert + i*nVert firstDiag = nHorz*nVert + (nHorz + 1)*nVert + (i-1)*nVert for j = 1 to nVert nxt = j%nVert + 1 pgon 3, 0, 0, firstHorz + j, firstVert + nxt, -(firstDiag + j) pgon 3, 0, 0, firstDiag + j, -(firstHorz + nVert + j), -(firstVert + j) next j next i
We’re almost done, but we still need to put the lid on the sphere, so add the last few polygons as follows (figure 292). !Polygons around the North Pole firstHorz = (nHorz - 1)*nVert firstVert = nHorz*nVert + nHorz*nVert for j = 1 to nVert nxt = j%nVert + 1 pgon 3, 0, 0, firstHorz + j, firstVert + nxt, -(firstVert + j) next j
Chapter 20: Freeform 3D Modeling - 3D Modeling 345
20.4.4
Body
We may as well complete the process by forming the solid body. Add this script to your 3D Script. !Body body -1
Well, here’s the finished product(figure 292). The trouble is, it doesn’t look finished at all. For one thing, it’s quite lumpy. Despite all those vertices, it doesn’t look smooth at all. And it’s covered in lines. If you like you can check that the body is truly solid. Place an instance of the object in the plan view (you may want to add a simple 2D Script like ‘circle2 0, 0, R’ before placing it). Open the 3D window, and use cutting planes or solid element operations Figure 292 – By adding the command ‘body -1’ we’ve now converted the to slice a piece off the sphere. If the sphere is solid, you’ll see the cut planes.
20.4.5
Edge and Polygon Status Values
To get rid of the lines and the lumpiness, look back through your script. Every time you find an edge definition, change the final zero to a 2. For example, change this:
surface to a solid body.Mission complete – almost. It remains to change the lumpy, golfball-like surface to a smooth sphere.
edge firstVertex + j, firstVertex + nVert + nxt, -1, -1, 0
to this: edge firstVertex + j, firstVertex + nVert + nxt, -1, -1, 2
For each polygon definition, change the third value to 2. For example, change this: pgon 3, 0, 0, firstHorz + j, firstVert + nxt, -(firstDiag + j)
to this: pgon 3, 0, 2, firstHorz + j, firstVert + nxt, -(firstDiag + j)
Run the script again and you’ll get a far smoother looking sphere (figure 293). By changing the edge and polygon status values to 2 , we have instructed the GDL interpreter that this is a curved body and each edge joins two polygons on a curved Figure 293 – By changing the edge and polygon status values, we can obtain a surface. smooth surface.
20.4.6
Texture Mapping
The one thing we haven’t yet considered in this example is the texture mapping (figure 294). You could cheat, and set the texture mapping method to 4 in the coor
346 Chapter 20: Freeform 3D Modeling - 3D Modeling command to force a spherical mapping. Because this is only a throw-away example, we’ll go ahead and use this simplistic approach. Add the following lines of script before the body -1 command. !Texture Mapping base vert 0, 0, 0 vert R, 0, 0 vert 0, R, 0 vert 0, 0, R coor 4 + 1024, -1, -2, -3, -4
Add a material parameter to the object, and use it to select a textured material Try a 3D View with the texture mapping in effect (figure 295), then comment the Figure 294 – If you apply a texture to coor statement out and try the 3D View again. You’ll see the difference in the way the sphere, you’ll see that there is a bit the texture is mapped onto the sphere. of a problem.
20.4.7
Notes
Typically, curved surfaces require a relatively high polygon count. We can use primitives to precisely control the number of polygons. If you define polygons in the wrong direction, the shading will look peculiar, as if you are seeing the inside of the body from the outside.
Figure 295 – In this case we’ll simply use the built-in spherical texture mapping method.
Chapter 20: Freeform 3D Modeling - Freeform Modeling 347
20.5 Freeform Modeling In the previous section we used primitives to construct a sphere. The first step in that process was to define a set of vertices. We defined the vertices as a series of circular rings, each ring lying on a different radius so as to form a sphere. Had we used different shapes for the rings, we would have ended up with a different result. Imagine if the rings had not been round but square. Then our body could have ended up looking something like that shown in figure 296. Similarly, there is no reason why the ring radii should follow a spherical path. A very small change to the 3D Script would form the dish shape of figure 297.
Figure 296 – A ‘sphere’ with square
Surfaces may be defined either mathematically as in the examples above, or from latitudes. measured data. The set of mathematical forms can be extended. For example, we could use splines to make a mouldable ‘blob’ based on the sphere, or mouldable tubes, cylinders and cones. Branching elements such as trees or animal forms could be added. Freeform modeling is perhaps the most exciting, challenging and relatively unexplored ‘final frontier’ of GDL scripting. In this chapter, we’ll look at just one example of a highly adjustable mathematical form. We’ll construct a rubberized version of the block element. Our rubbery block will be based on a smoothly curved surface defined by a grid of quadratic Bezier splines. All the control points will be editable via dynamic hotspots. A thickness will be applied to calculate a top surface and complete the solid body.
20.5.1
Defining the Surface
Create a new object called Rubbery Block and add a length-type parameter rib.
Figure 297 – A small change to the sphere 3D script will result in a bowl.
Click on the Array button and make the parameter a 2-dimensional array with 9 rows and 3 columns (figure 298). Each set of 3 rows will define an ‘rib’ of the rubbery block – the start point, a central control point and an end point. The ribs run across the block, beginning to define its curved surface. To see the control points (figure 299), copy the script below into the 3D Script Figure 298 – Enter some control point editing window. co-ordinates into the rib[][] array.
348 Chapter 20: Freeform 3D Modeling - Freeform Modeling resol 6 !Display the rib nodes (debugging only) for i = 1 to 9 add rib[i][1], rib[i][2], rib[i][3] sphere .010 del 1 next i
A regular Bezier spline runs tangentially to the lines joining the end points to the midpoint, but we want our splines to pass through the central control points. We must adjust the midpoints. To do so, add the following script to the end of the 3D Script. !Adjust the central rib control points !so that the splines will pass through the original points
Figure 299 – The control points shown as small spheres.
for i = 1 to 3 i1 = 3*(i - 1) + 1 i2 = 3*(i - 1) + 2 i3 = 3*(i - 1) + 3 for j = 1 to 3 rib[i2][j] = 2*rib[i2][j] - (rib[i1][j] + rib[i3][j])/2 next j next i
Now we are ready to create the ribs as a series of points defined by splines through the control points. For each point, we’ll also take the time to calculate the direction vector of the curve at that point. The set of ith points (one on each of the three ribs) will become the control points for a spline (we’ll call it a spine to distinguish it from a rib) running across the ribs. !Spine co-ordinates and direction vectors dim spineX[][], spineY[][], spineZ[][], spineUX[][], spineUY[][], spineUZ[][] dt = 1/10
Figure 300 – Splines are interpolated through the control points.
Chapter 20: Freeform 3D Modeling - Freeform Modeling 349 for i = 1 to 3 i1 = 3*(i - 1) + 1 i2 = 3*(i - 1) + 2 i3 = 3*(i - 1) + 3 nSpines = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t nSpines = nSpines + 1 spineX[nSpines][i] = rib[i1][1]*s^2 + 2*rib[i2][1]*s*t + rib[i3][1]*t^2 spineY[nSpines][i] = rib[i1][2]*s^2 + 2*rib[i2][2]*s*t + rib[i3][2]*t^2 spineZ[nSpines][i] = rib[i1][3]*s^2 + 2*rib[i2][3]*s*t + rib[i3][3]*t^2 spineUX[nSpines][i] = 2*(rib[i2][1] - rib[i1][1]) + 2*t*(rib[i3][1] - 2*rib[i2][1] + rib[i1][1]) spineUY[nSpines][i] = 2*(rib[i2][2] - rib[i1][2]) + 2*t*(rib[i3][2] - 2*rib[i2][2] + rib[i1][2]) spineUZ[nSpines][i] = 2*(rib[i2][3] - rib[i1][3]) + 2*t*(rib[i3][3] - 2*rib[i2][3] + rib[i1][3]) !Display the spine control points (Debugging only) add spineX[nSpines][i], spineY[nSpines][i], spineZ[nSpines][i] sphere .005 del 1 next t next i
Now we have a set of control points for splines running across the three ribs (figure 300), so we are ready to calculate the complete surface as a grid of points. For each grid point, we can directly calculate the direction vector along the spine, and we can interpolate values for the direction vector along the rib direction. To calculate the normal vectors at each grid point, we’ll use the cross product of the spine and rib direction vectors. !Calculate all the points, including all direction vectors dim x[][], y[][], z[][], ux[][], uy[][], uz[][], vx[][], vy[][], vz[][], wx[][], wy[][], wz[][]
350 Chapter 20: Freeform 3D Modeling - Freeform Modeling for i = 1 to nSpines spineX[i][2] = 2*spineX[i][2] - (spineX[i][1] + spineX[i][3])/2 spineY[i][2] = 2*spineY[i][2] - (spineY[i][1] + spineY[i][3])/2 spineZ[i][2] = 2*spineZ[i][2] - (spineZ[i][1] + spineZ[i][3])/2 spineUX[i][2] = 2*spineUX[i][2] - (spineUX[i][1] + spineUX[i][3])/2 spineUY[i][2] = 2*spineUY[i][2] - (spineUY[i][1] + spineUY[i][3])/2 spineUZ[i][2] = 2*spineUZ[i][2] - (spineUZ[i][1] + spineUZ[i][3])/2 j = 0 for t = 0 to 1 step dt s = 1 - t j = j + 1 x[i][j] = spineX[i][1]*s^2 + 2*spineX[i][2]*s*t + spineX[i][3]*t^2 y[i][j] = spineY[i][1]*s^2 + 2*spineY[i][2]*s*t + spineY[i][3]*t^2 z[i][j] = spineZ[i][1]*s^2 + 2*spineZ[i][2]*s*t + spineZ[i][3]*t^2 ux[i][j] = spineUX[i][1]*s^2 + 2*spineUX[i][2]*s*t + spineUX[i][3]*t^2 uy[i][j] = spineUY[i][1]*s^2 + 2*spineUY[i][2]*s*t + spineUY[i][3]*t^2 uz[i][j] = spineUZ[i][1]*s^2 + 2*spineUZ[i][2]*s*t + spineUZ[i][3]*t^2 vx[i][j] = 2*(spineX[i][2] - spineX[i][1]) + 2*t*(spineX[i][3] - 2*spineX[i][2] + spineX[i][1]) vy[i][j] = 2*(spineY[i][2] - spineY[i][1]) + 2*t*(spineY[i][3] - 2*spineY[i][2] + spineY[i][1]) vz[i][j] = 2*(spineZ[i][2] - spineZ[i][1]) + 2*t*(spineZ[i][3] - 2*spineZ[i][2] + spineZ[i][1]) wx[i][j] = uy[i][j]*vz[i][j] - uz[i][j]*vy[i][j] wy[i][j] = uz[i][j]*vx[i][j] - ux[i][j]*vz[i][j] wz[i][j] = ux[i][j]*vy[i][j] - uy[i][j]*vx[i][j] Li = sqr(wx[i][j]^2 + wy[i][j]^2 + wz[i][j]^2) wx[i][j] = wx[i][j]/Li wy[i][j] = wy[i][j]/Li wz[i][j] = wz[i][j]/Li !Display the nodes, direction vectors and !normal vectors (Debug Only) add x[i][j], y[i][j], z[i][j]
Chapter 20: Freeform 3D Modeling - Freeform Modeling 351 sphere .005 lin_ 0, 0, 0, .05*ux[i][j], .05*uy[i][j], .05*uz[i][j] lin_ 0, 0, 0, .05*vx[i][j], .05*vy[i][j], .05*vz[i][j] lin_ 0, 0, 0, .05*wx[i][j], .05*wy[i][j], .05*wz[i][j] del 1 next t nNodes = j next i
It looks like we’ve successfully created a rubberized bed of nails (figure 301)!
20.5.2
Vertices
Now it’s time to start laying down the vertices. We’ll create the vertices as sets of closed paths. Each set will run across the spines to form one grid row of the lower surface, then back across the spines to form the corresponding row of the upper surface (figure 302).
Figure 301 – A rubberized bed of nails?
!Vertices body -1 base for j = 1 to nNodes for i = 1 to nSpines vert x[i][j], y[i][j], z[i][j] !
add x[i][j], y[i][j], z[i][j]
!
sphere .005
!
del 1 next i for i = nSpines to 1 step -1 vert x[i][j] + blockThick*wx[i][j], y[i][j] + blockThick*wy[i][j], z[i][j] + blockThick*wz[i][j]
!
add x[i][j] + blockThick*wx[i][j],
!
y[i][j] + blockThick*wy[i][j],
! ! !
z[i][j] + blockThick*wz[i][j] sphere .005 del 1 next i
Figure 302 – The vertices are defined in sets that run across the lower surface then back across the upper surface.
352 Chapter 20: Freeform 3D Modeling - Freeform Modeling next j
20.5.3
Edges
As with the sphere we constructed earlier, we’ll use a system of horizontal, vertical and diagonal edges. We’ll start by constructing the horizontal loops (figure 303). !Edges nHorz = nNodes nVert = 2*nSpines !Horizontals for i = 1 to nHorz firstVertex = (i - 1)*nVert for j = 1 to nVert
Figure 303 – Our first set of edges forms loops around the surface.
s = 2 if i = 1 then s = 0!10 if i = nHorz then s = 0!10 nxt = j%nVert + 1 edge firstVertex + j, firstVertex + nxt, -1, -1, s next j next i
The verticals include edges on the initial horizontal loop (figure 304). !Verticals !Verticals across the Base for j = 2 to nVert/2 - 1 edge j, nVert + 1 - j, -1, -1, 2
Figure 304 – Edges across the ‘base’.
next j
Then there are a bunch of regular verticals (figure 305). !Regular verticals for i = 1 to nHorz - 1 firstVertex = (i - 1)*nVert for j = 1 to nVert s = 2 if j = 1 then s = 0!10 if j = nVert/2 then s = 0!10 if j = nVert/2 + 1 then s = 0!10
Figure 305 – ‘Vertical’ edges.
Chapter 20: Freeform 3D Modeling - Freeform Modeling 353 if j = nVert then s = 0!10 edge firstVertex + j, firstVertex + nVert + j, -1, -1, s next j next i
Finally, there are the vertical edges across the last horizontal loop (figure 306). !Verticals across the Top firstVertex = (nHorz - 1)*nVert for j = 2 to nVert/2 - 1 edge firstVertex + j, firstVertex + nVert + 1 - j, -1, -1, 2
Figure 306 – Edges across the ‘top’.
next j
Similarly, the diagonals include edges on the first and last horizontal loops (figure 307). !Diagonals !Diagonals Across the Base for j = 1 to nVert/2 - 1 edge j, nVert - j, -1, -1, 2 next j !Diagonals on the Main Surface for i = 1 to nHorz - 1 firstVertex = (i - 1)*nVert for j = 1 to nVert nxt = j%nVert + 1 edge firstVertex + j, firstVertex + nVert + nxt, -1, -1, 2 next j next i !Diagonals Across the Top firstVertex = (nHorz - 1)*nVert for j = 1 to nVert/2 - 1 edge firstVertex + j, firstVertex + nVert - j, -1, -1, 2 next j
Figure 307 – ‘Diagonal’ edges across top, base and the main surface.
354 Chapter 20: Freeform 3D Modeling - Freeform Modeling
20.5.4
Polygons
Now all the edges have been constructed, its time to add the polygons. We’ll start by constructing polygons to fill the first horizontal loop, forming one ‘end’ of the rubbery block (figure 308). This is a bit tricky, as in most cases we will be using the verticals, but at the ends we will use the horizontals. !Polygons !Polygons across the Base firstHorz = 0 firstVert = nHorz*nVert firstDiag = nHorz*nVert + nVert/2 – 2 + (nHorz - 1)*nVert + nVert/2 - 2
Figure 308 – Polygons across the ‘base’.
for j = 1 to nVert/2 - 1 if j < nVert/2 - 1 then pgon 3, 0, 2, firstHorz + j, firstVert + j, -(firstDiag + j) else pgon 3, 0, 2, firstHorz + j, firstHorz + j + 1, -(firstDiag + j) endif if j = 1 then pgon 3, 0, 2, firstDiag + j, firstHorz + nVert - j, firstHorz + nVert else pgon 3, 0, 2, firstDiag + j, firstHorz + nVert - j, -(firstVert + j - 1) endif next j
The regular polygons are more straightforward (figure 309), but even here we need to be careful to define the polygons in the correct orientation. !Regular Polygons for i = 1 to nHorz - 1 firstHorz = (i - 1)*nVert firstVert = nHorz*nVert + (nVert/2 - 2) + (i - 1)*nVert
Chapter 20: Freeform 3D Modeling - Freeform Modeling 355 firstDiag = nHorz*nVert + 2*(nVert/2 - 2) + (nHorz-1)*nVert + (nVert/2 - 1) + (i - 1)*nVert for j = 1 to nVert nxt = j%nVert + 1 pgon 3, 0, 2, -(firstHorz + j), firstDiag + j, -(firstVert + nxt) pgon 3, 0, 2, -(firstDiag + j), firstVert + j, firstHorz + nVert + j next j next i
The final set of polygons forms the far end of the rubbery block (figure 310). This block of script is similar to that used for the near end, but the polygon orientation must be reversed.
Figure 309 – Polygons to the main surface.
!Polygons across the top firstHorz = (nHorz - 1)*nVert firstVert = nHorz*nVert + (nVert/2 - 2) + (nHorz - 1)*nVert firstDiag = nHorz*nVert + (nVert/2 - 2) + (nHorz - 1)*nVert + (nVert/2 - 2) + (nVert/2 - 1) + (nHorz - 1)*nVert for j = 1 to nVert/2 - 1 if j < nVert/2 - 1 then pgon 3, 0, 2, -(firstHorz + j), firstDiag + j, -(firstVert + j) else pgon 3, 0, 2, -(firstHorz + j), firstDiag + j, -(firstHorz + j + 1) endif if j = 1 then pgon 3, 0, 2, -(firstDiag + j), -(firstHorz + nVert), -(firstHorz + nVert - j) else pgon 3, 0, 2,
Figure 310 – Polygons to the ‘top’ complete a closed surface.
356 Chapter 20: Freeform 3D Modeling - Freeform Modeling -(firstDiag + j), firstVert + j - 1, -(firstHorz + nVert - j) endif next j
20.5.5
Curved or Flat Surfaces?
In this example, you can see how the curved surfaces have been shaded to give the effect of a smoothed surface (figure 311). Even where the edges have been defined as visible (i.e. a mask of 0), the smoothing is applied. The reason for this is that the smoothing criterion between two adjacent polygons is based on the polygon masks rather than the edge masks.
Figure 311 – The whole surface is
Due to the limited mask options for polygons, it is not possible to define a polygon that curved, including the ‘sharp’ forms a sharp edge with one of its neighbors, and a smooth edge with another. A polygon edges. is either part of a curved surface (mask = 2), flat (mask = 0), or invisible (mask = 1). To provide a clean, sharp edge to our rubber sheet, we must change the edge polygons to flat polygons. By changing the status of the edge polygons from 2 to 0, we obtain a slightly more satisfactory result as illustrated (figure 312). It’s OK to use flat polygons and curved edges together in the same body. If you do so, you should bear one thing in mind. Any body containing a curved edge must also contain at least one curved polygon, otherwise an error message will pop up every time the body is generated.
20.5.6
Body
The final step is to define a body. body -1
Figure 312 – To obtain sharp edges, we must sacrifice any curvature on the edge polygons.
Chapter 20: Freeform 3D Modeling - Advanced Texture Mapping 357
20.6 Advanced Texture Mapping Sadly, the basic texture maps are inadequate for freeform surfaces and even for some regular 3D elements. Look at how the rectangular coor option maps a timber texture onto a slide created using the tube element (figure 313). For better control over the way texture is mapped to a surface, you need to use GDL primitives, and define the vertices using the texture vertex teve in place of the regular vertex vert. A teve element is a vertex that includes texture co-ordinates. Each teve Figure 313 – Texture mapping on a tube. maps a point on the 3D surface to a point on the texture. For each polygon on the surface, the texture is stretched between the vertices defining that polygon. Try it Yourself: Map a Texture onto a Rectangular Polygon As an example, we’ll map a texture onto a rectangular polygon with vertices at (0, 0, 0), (1, 0, 0) and (1, 1, 0) and (0, 1, 0). In the ArchiCAD Options menu, choose Element Attributes > Materials Figure 314 – Open the Material Settings dialog. (figure 314). Select a material from the list and choose a texture image for that material (figure 315). Set the texture image size to be square, even if this causes some distortion. You may have to disable the Keep Original Proportion checkbox. Create a new object, and add a material-type parameter with variable textureMat. For the default value, select the material you just edited. Copy the following script into its 3D Script editing window. !Set the material material textureMat !Vertices with texture mapping base teve 0, 0, 0, 0, 0 teve 1, 0, 0, 1, 0 teve 1, 1, 0, 1, 1 teve 0, 1, 0, 0, 1
Figure 315 – Choose a texture image for one of the materials. Set the image size to be square.
358 Chapter 20: Freeform 3D Modeling - Advanced Texture Mapping !Edges edge 1, 2, -1, -1, 0 edge 2, 3, -1, -1, 0 edge 3, 4, -1, -1, 0 edge 4, 1, -1, -1, 0 !Polygon pgon 4, 0, 0, 1, 2, 3, 4 !Texture co-ordinate system base teve 0, 0, 0, 0, 0 teve 1, 0, 0, 1, 0 teve 0, 1, 0, 0, 1 teve 0, 0, 1, 0, 1 !Texture mapping method coor 1 + 1024, -1, -2, -3, -4
Figure 316 – The texture is mapped tidily onto the polygon.
!Body body -1
Click on the 3D View button to run the script (figure 316). Let’s examine the script. If you look at the block titled !Texture co-ordinate system, you’ll see that we’ve defined four vertices, one for the origin of the co-ordinate system, and one each for the three axis dimensions. In order for the texture mapping to work, even these vertices must be defined as teves, even though they do not form part of the surface. Now let’s look at the block of script titled !Vertices with Texture Mapping. This block of script lists the vertices used to define the shape of the polygon. Each vertex is mapped to a point in the texture. If you look at the block of script headed !Texture mapping method, you’ll see that the method 1024 + 1 was used. This is the best method for mapping with texture vertices.
20.6.1
Mapping a Texture to a Polygon
In our example, try changing the vertex definitions in the 3D Script as follows. teve 0, 0, 0, 0, 0 teve 1, 0, 0, 1, 0
Figure 317 – By adjusting the texture u, v co-ordinates we can distort the texture.
Chapter 20: Freeform 3D Modeling - Advanced Texture Mapping 359 teve 1, 1, 0, 1, 0.5 teve 0, 1, 0, 0, 1
Now the texture is distorted (figure 317). It is stretched out along the right hand edge of the polygon. If we wanted to fit exactly one texture image onto the polygon, we would have to set the texture co-ordinates to match the dimensions of the texture picture (figure 318). In our example, the texture picture was set at 700mm x 700mm. Change the script to read: teve 0, 0, 0, 0, 0 teve 1, 0, 0, 0.7, 0 teve 1, 1, 0, 0.7, 0.7 teve 0, 1, 0, 0, 0.7
Figure 318 – To map exactly one
Of course, the size of the texture image for any given material is a part of the material image onto the surface, we must settings, and there is currently no GDL request to obtain this information. know the size of that image as
20.6.2
Mapping a Texture to a Solid Body
stored in the material definition.
When you map a texture to a solid body, it’s a bit more tricky, because each vertex is shared by multiple polygons. In some cases, textures can be wrapped around a body, with the texture co-ordinates increasing until the half-way point is reached, then decreasing to the final point. This approach can be useful for tube and pocket type bodies (figure 319). Every vertex on a horizontal loop has the same value for the texture B co-ordinate, and every vertex on a given vertical loop has the same value for the texture A co-ordinate. A simple cuboid or prism can never have a satisfactory texture mapped to all of its Figure 319 – For this pocket type surfaces. You can wrap a texture around four sides of the cuboid, but the ends are always a body, texture mapping can be problem (figure 320). achieved quite successfully.
Try it Yourself: Map a Texture to a Cuboid Try it for yourself. Create a new object called Cuboid and add a material-type parameter with variable blockMat. Copy the following script into its 3D Script editing window. resol 6 material blockMat !Vertices base
360 Chapter 20: Freeform 3D Modeling - Advanced Texture Mapping teve 0, 0, 0, 0, 0 teve a, 0, 0, a, 0 teve 0, 0, zzyzx, 0, zzyzx teve a, 0, zzyzx, a, zzyzx teve 0, b, zzyzx, 0, zzyzx + b teve a, b, zzyzx, a, zzyzx + b teve 0, b, 0, 0, b teve a, b, 0, a, b !Edges edge 1, 3, -1, -1, 0 edge 3, 5, -1, -1, 0 edge 5, 7, -1, -1, 0 edge 7, 1, -1, -1, 0 edge 2, 4, -1, -1, 0 edge 4, 6, -1, -1, 0 edge 6, 8, -1, -1, 0 edge 8, 2, -1, -1, 0 edge 1, 2, -1, -1, 0 edge 3, 4, -1, -1, 0 edge 5, 6, -1, -1, 0 edge 7, 8, -1, -1, 0 !Polygons pgon 4, 0, 0, 1, 2, 3, 4 pgon 4, 0, 0, -8, -7, -6, -5 pgon 4, 0, 0, 9, 5, -10, -1 pgon 4, 0, 0, 10, 6, -11, -2 pgon 4, 0, 0, 11, 7, -12, -3 pgon 4, 0, 0, 12, 8, -9, -4 !Texture co-ordinates base
Chapter 20: Freeform 3D Modeling - Randomizing Objects 361 teve 0, 0, 0, 0, 0 teve 1, 0, 0, 1, 0 teve 0, 1, 0, 0, 1 teve 0, 0, 1, 0, 1 coor 1024 + 1, -1, -2, -3, -4 body -1
For this type of element, just use the regular cube texture mapping method 2. There will always be some forms that do not map texture well on the end surfaces. In such cases, you can choose to ignore the problem, or if you are very fussy I guess you could Figure 320 – Mapping a texture place an independent polygon at the end using its own separate set of vertices.
20.7 Randomizing Objects
to a cuboid is not as simple as you would expect.
This section on randomizing could apply to any object, not just those that use primitive elements. I’ve included it here because randomization and freeform modeling are often associated with natural forms. Since two natural forms are rarely identical, it is usually necessary to provide some differences between any two instances of the object. The process of randomizing an object is relatively straightforward. Every aspect of the object can be described via the value of either a scripted variable or a user-defined parameter. By varying these values by a random amount, the shape of the object will automatically change.
20.7.1
The rnd( ) Function
You can generate a random number within a GDL script by using the rnd( ) function. This function takes a single argument, and returns a random number. The value of the argument defines the upper limit of the returned value. The lower limit is zero. Thus, to assign a random number between 0 and 1 to the variable randomNumber, simply use the expression: randomNumber = rnd(1)
To see how this works, create a new object and type the following GDL script into the 3D Script editing window. for i = 1 to 10 print rnd(1) next i
Now run the 3D Script by clicking on the 3D View button. Provided you have not turned off error messages, a series of alert messages will pop up, each one displaying a different random number with a value somewhere between 0 and 1.
20.7.2
Using the rnd( ) Function Practically
There are a number of ways that the rnd( ) function can be used to adjust values.
362 Chapter 20: Freeform 3D Modeling - Randomizing Objects
20.7.3
Forcing an Object to Update on Placing
When you place two objects with the exact same parameter set, the GDL interpreter is a bit lazy in a clever sort of way. It recognizes that the parameter sets are identical, and doesn’t bother re-constructing the 2D symbol, because it thinks it doesn’t need to! Normally this is a great feature, as it cuts down on unnecessary processing, but when you’re trying to randomize the objects it can be frustrating. It turns out that the GDL interpreter is smart enough to know which global variables an object uses. We can use this to force an object to update whenever a new instance of it is placed into the project. If we use the global position variables symb_pos_x, symb_pos_y and symb_pos_z in the GDL script, the object will automatically update whenever it is placed or moved. Effectively, placing or moving the object will update the values of these variables, just as changing a parameter value. As a result, the object runs its 2D Script or 3D Script whenever it is moved. In practice, force the object to update by typing the following GDL script into its Master Script editing window. posX = symb_pos_x posY = symb_pos_y posZ = symb_pos_z
20.7.4
Controlled Randomization
While it’s great to randomize each instance of an object, it can also be handy to be able to reproduce a specific randomization. Imagine that you are placing random trees into a project. By chance, one of them turns out to be the spitting image of an existing tree that dominates the landscape of a different project. Wonderful! You quickly save the tree as a module file, and merge it into the second project. To your disappointment, however, when the tree is merged into the second project, it gets re-randomized and looks totally different. The reason that the tree looks different when merged into the second project is that the process of merging it gives it a new unique identifier. Since the unique identifier is used as a seed for the randomization algorithm, the new tree gets a different set of random numbers. One way to solve this problem is to calculate a new set of random numbers on demand, and then to store them in an array parameter. This way, we can re-produce the exact same shape by copying the values from one object to another (for example, by creating a copy of the original object). To make this work, we need rules around when a new list of random numbers should be generated. One way to approach this is to provide a checkbox parameter. When the checkbox is turned on, the object will store a new set of parameters whenever a new instance is placed. When the checkbox is turned off, it will stop storing the parameters.
Chapter 20: Freeform 3D Modeling - Trouble Shooting 363
20.8 Trouble Shooting Working with primitives requires good planning, a structured, methodical approach and intense concentration. A few techniques that I find useful include: •
Always have a clear understanding of what you are aiming for, and how you intend to get there.
•
Take time to visually check the locations of vertices before moving on to the edges. Vertices are invisible, making it difficult to get visual feedback on whether they have been placed correctly. Since everything else depends on the vertex location, it’s worth checking this before moving on to edges and eventually polygons. To visually check vertex locations, substitute a small sphere for each vertex.
•
Place the vertices in a strict order, and check there are no repeated vertices. To check the order, limit the number of placed vertices (spheres) – for example, if you are using a for – next loop to place the vertices, change the end conditions temporarily to limit the number of vertices to just one (to get a handle on the first vertex placed), then two or three (to get an idea of the order in which they are placed), then a complete layer, then two or three layers, then all but one layer, and finally the whole shooting box.
•
When you define edges, be aware of the edge direction.
•
Break edges into logical groups. For simple shapes I tend to use vertical, horizontal and diagonal edges.
•
The edge status values should also be considered.
Creating polygons is perhaps the most difficult stage of all. The edges must be listed in the correct order and with the correct direction. Remember that the outward normal should always point out of the body, so use the right-hand rule to check this if you are in any doubt. Despite your best efforts, if you are human you will run into difficulties quite frequently. Persistence and meticulous ordering are what will give you the final victory. And like most skills, the process will get easier each time you work through it.
21 Polygon Operations Working with polygons is a major part of GDL. In fact it is one of the single, biggest challenges for GDL programmers. Most of the objects you create will probably involve polygons. A polygon might, for instance, be used to define the shape of a window, a cabinet, or a profiled scotia. It is often important to be able to offset, split, add, subtract and otherwise manipulate these polygons. The shape of a window must be offset to define a frame, then split to create mullions. Shelves inside a cabinet must be offset by a different amount from each edge (front, back, sides) of the cabinet to allow for the end panels, the back of the cabinet, and an inset from the front. To create a wall frame from a given wall surface polygon, you may need to intersect polygons representing studs and plates with the wall polygon, or subtract them from the wall polygon. In this chapter we will deal only with polygons that have straight edges - not so big a sacrifice as it might seem at first. As we saw in a previous chapter, any curve can be approximated by a linked set of line segments, and we can apply this principle successfully to polygons too. However this approach tends to produce polygons with large numbers of edges, resulting in a perceptible drop-off in performance when multiple polygons are involved. I will leave it to the reader to extend the algorithms to include edges that may be either straight or circular arcs. In the first section of the chapter, we’ll precisely define what we mean by a polygon and choose a method to store polygon data. We’ll then look at a number of key polygon operatations, and develop re-usable sub-routines to implement them. The sub-routines in this chapter provide a foundation for working with polygons. With a little work, they can be tailored to suit specific needs. For example, the sub-routine for offsetting edges allows each edge to be offset by a different amount. Two variations of this subroutine immediately spring to mind. One of these might offset all the edges by the same amount, the other might offset flagged groups of edges by given amounts. You should find it easy enough to create these variations by copying the “Offset Polygon” algorithm, re-naming it, and making a few changes. I’ll finish up by discussing some improvements that can be made to enhance the performance of the sub-routines. Much of this work could be carried out using group operations. However, group operations will only go part way towards a solution, as they do not provide information about the resulting polygon edge co-ordinates. It is often important to know edge co-ordinates for quantity calculation or to place other elements adjacent to edges. Many of you will also be aware of the polygon processing add-on provided by Graphisoft. While this add-on is also an excellent resource, I personally favour a macro-based approach, as I can more easily tailor the macros to my requirements.
21.1 Defining a Polygon Before we start working with polygons, we need to define exactly what we mean by the term polygon. This is essential if we want to compare or perform operations between two polygons.
Chapter 21: Polygon Operations - Polygon Area 365 We will define a polygon as a set of edges. Each edge is defined by two vertices, a start point and an end point. For any given polygon, we will list the edges in order, so that the end point of one edge is the start point of the next. The list of edges is closed, so that the end point of the last edge is the start point of the first edge. The minimum data required to store a polygon is the list of edge start points. It is also convenient to store the number of edges. A polygon can be stored in an array variable. In this chapter we will be working with algorithms that operate on multiple polygons and may return multiple polygons, so it will be convenient to store multiple polygons in two arrays. We will use array variables pgonX[][] and pgonY[][] to store the polygons. Each row of these two arrays will hold a list of the x and y co-ordinates of the start point of each edge of a polygon. Thus, the start co-ordinates of the 5th edge of polygon 3 will be (pgonX[3][5], pgonY[3][5]). The end co-ordinates of the same edge are the start co-ordinates of the next edge – provided there are more than 5 edges in the polygon, the edge end co-ordinates are then (pgonX[3][6], pgonY[3][6]). We will use an integer array pgonN[] to store the number of edges in each polygon. We noted that the end co-ordinates of an edge are identical to the start co-ordinates of the next edge. However, in this context we should be careful to define what we mean by the term next. Normally for a given edge j, we would expect the next edge to have the index j + 1. This is not always true. When we consider the last edge in the polygon, its end point is in fact the start point of the first edge. Rather than calculating the end point (or the next point in a polygon) as j + 1, we will use j%n + 1 where n represents the number of edges in the polygon. We first take the modulus (base n) of the current edge, then increment this value. Of course, if j < n, this is identical to simply incrementing j. When j = n, the formula still works, as n%n + 1 = 0 + 1 = 1. It may be useful to store other data about each edge. For example, in certain applications we may wish to store data relating to the edge’s visibility, or a pen or material index. In other applications it is useful to store the direction vectors of the edges. These extra data can be stored in independent array variables. You will need to keep track of the extra data within the polygon operation sub-routines. Rather than putting whole polygon definitions on the stack, we will use working polygon arrays that are global to the script. This approach deviates from our general rules of re-usable sub-routines, but by differentiating between working and stored polygons we can ensure the scripts maintain their integrity.
21.2 Polygon Area When dealing with polygons, it is often necessary to calculate the area of a polygon. Oddly, it is easier to calculate a polygon’s area than it is to check its sense (direction of circulation). A polygon is considered to have a positive area if the edges run in a counter-clockwise sense. Otherwise the polygon has a negative area.
366 Chapter 21: Polygon Operations - Polygon Area Rather than thinking of the polygon as clockwise or counter-clockwise, it might be easier to imagine that the polygon is a race track. As you race around the edges of a positive polygon, the polygon surface will always be on your left. In the case of a negative polygon, the surface will always be on your right (figure 321). This is easy enough to visualize, and you can easily ensure that your scripts define polygons in a consistent sense. Imagine a polygon in the x-y plane. For each edge, construct a new edge that extends from the origin to the start point, and another new edge extending from the end point back to the origin (figure 322). The three edges form a triangle which may have positive or negative area, depending on the sense of the triangle. In figure 323, I’ve marked in all the triangles. Triangles with negative area have Figure 321 – As you race around the been given a cross-hatched fill. As you can see, if you add the positive and perimeter of a polygon with positive area, the negative areas together, the sum is the area of the original polygon. polygon surface will always be on your left. To calculate the area of one of the triangles we can use the cross product formula (figure 324). This yields the area of the parallelogram, exactly twice the are of the triangle. To get the triangle area then, we simply divide by 2.
= A 1
2∑
u= × vi 1 i
2∑
xi × yi +1 − xi +1 yi
The GDL script might look something like this: "20 Polygon Area": !Input: ! - a polygon pgonX[i~20][], pgonY[i~20][], with pgonN[i~20] edges ! (This polygon is global to the GDL script, so does not need re! dimensioning when it is to be used. It is only a temporary working ! polygon - as soon as the calculation is complete the results should be !
re-assigned to the real polygon).
!Return: !
- the area of a polygon area~20
!Calculation: area~20 = 0
Figure 322 – For each edge, construct a triangle with sides formed by that edge and extensions back to the origin.
Chapter 21: Polygon Operations - Polygon Area 367 for j~20 = 1 to pgonN[i~20] nxt~20 = j~20 % pgonN[i~20] + 1 area~20 = area~20 + pgonX[i~20][j~20]*pgonY[i~20][nxt~20] – pgonX[i~20][nxt~20]*pgonY[i~20][j~20] next j~20 area~20 = area~20/2 return
21.2.1
Surface Area of a Body
In the previous chapter, we considered how to construct bodies using a surface mesh of polygons. We also noted that any body can be constructed using Figure 323 – Construct triangles for all the triangular polygons exclusively. edges. The polygon area can be found by
Were we to keep track of the edges used for each of these polygons, we could adding these positive and negative areas. easily calculate the surface area of the body as:
= A
= A ∑ i
1
2∑
pi1 × pi 2
where the second equality is true only for triangular polygons.
21.2.2
Volume of a Body
A natural extension of our approach to calculating the area of a polygon, is an algorithm to calculate the volume of a body defined as a set of polygons. In the area calculation, we added the areas of triangular regions formed by the start and end points of each edge, and the origin (0, 0). The areas added and subtracted to result in the total area enclosed by the edges. In the same way, we can add the volumes of pyramidal regions of 3-space formed by the areas enclosed by each of the surface polygons, the apex being the origin. We typically use triangular polygons for all bodies created using primitive elements, and this results in rather trivial area calculations.
= V 1
3∑
Ai pi ⋅ nˆi
For triangular regions, this reduces to
Figure 324 – The cross-product of the two exetension edges yields twice the area of the triangle for that edge.
368 Chapter 21: Polygon Operations - Ensure a Positive Polygon Sense
= V 1
6∑
pi ⋅ ( ui1 × ui 2 )
Where the vectors ui1 and ui 2 are the first two edges of the ith polygon, and pi the position vector of the first edge.
21.3 Ensure a Positive Polygon Sense In our area calculation, we started with a polygon whose edges ran in a counter-clockwise direction. The result of the area calculation was positive. If we were to reverse the sense of the polygon, our area calculation will result in a negative value. This gives us a simple test for the polygon sense. To test if a polygon is given in standard (positive) sense, calculate the area. If the area is positive, the sense is correct. Otherwise the polygon must be reversed to get a positive sense. The following sub-routine tests the sense of a given polygon and, if necessary, reverses the order of points to ensure a positive sense. “24 Ensure Polygon has Positive Sense”: !Input: !
- a polygon: pgonX[i~24][], pgonY[i~24][]
!
- number of edges on the polygon: pgonN[i~24]
!Return: !
- the standardized polygon.
!Note: !
The input and returned polygon is global to the script
!Calculation: !Calculate the polygon area i~20 = i~24 gosub “20 Polygon Area” !If the area is negative, reverse the order of points. if area~20 < 0 then !Copy the polygon to a new row in the pgonX[][], pgonY[][] arrays temp~24 = nPgons + 1 pgonX[temp~24] = pgonX[i~24] pgonY[temp~24] = pgonY[i~24] !Copy each co-ordinate pair back to the original row for j~24 = 1 to pgonN[i~24] k~24 = pgonN[i~24] + 1 - j~24 pgonX[i~24][j~24] = pgonX[temp~24][k~24] pgonY[i~24][j~24] = pgonY[temp~24][k~24]
Chapter 21: Polygon Operations - Check if a Point Lies Inside a Polygon 369 next j~24 endif return
Note how we copied the polygon to the first unused row of the existing arrays, rather than creating a new array to store it temporarily. It’s a good idea to minimize the number of array variables you use, while maintaining a readable script.
21.4 Check if a Point Lies Inside a Polygon To test whether or not a point (x0, y0) lies inside a polygon, we can extend a line directly up from the point (figure 325). If the line intersects a polygon edge that Figure 325 – Extend a line vertically starts to the right and ends at the left of the point, we add a unit (figure 326) If it from the point and add or subtract for intersects an edge that starts to the left and ends at the right of the point, we subtract a each edge that it intersects. unit. If the final sum comes to one unit, the point lies inside the polygon. In other words, for each edge (xj, yj) – (xnext, ynext), we should first check whether the end points bracket x0, i.e. check whether,
min(xi, xnext) ≤ x0 ≤ max(xi, xnext) then we should check whether the line is above yi at x = xi – i.e. check whether,
yi + (x0 – xi)(ynext – yi)/(xnext – xi) > y0 The solution looks simple enough, but there are some special cases. The first special case occurs whenever a point lies directly below the start or end point of an edge (figure 327) In this case we add or subtract a half unit, depending on which way the edge is traveling (right to left or vice versa). Figure 326 – If the final sum comes to
The second special case occurs when the point actually lies on one of the edges (figure one, then we know that the edge lies 328). Now is the edge above or below the point? If we say that the edge is above the within the polygon. point, then it will contribute. In the illustration, the point on the left will be counted as lying inside the polygon, while that on the right is outside. If we say the edge is below the point, then the point on the left will be counted as lying outside, while the point on the right is inside. We could provide rules for the inclusion of a point on an edge, but it is just as easy to test whether the point lies on an edge. In fact, it can be important to distinguish
370 Chapter 21: Polygon Operations - Check if a Point Lies Inside a Polygon between points that are strictly inside the polygon, and those that are on its boundary. If we find a point that lies on an edge, we will return this information, and say that the point is not strictly inside the polygon. Finally, there is the case of a vertical edge. In this case, the edge should not contribute to the sum. A re-usable sub-routine is given below. "25 Polygon Contains Point": !Input: !
- a point (xi~25, yi~25)
!
- a polygon i~25 = (pgonN[i~25], pgonX[i~25], pgonY[i~25]) if getValues then xi~25 = get(1): yi~25 = get(1)
Figure 327 – If the point lies directly below a vertex of the polygon, we add or subtract ½ for each of the two edges.
getValues = 0 endif !Return: !
- If the point lies within the polygon:
!
isWithin~25 = 1 (contained)
! !
isWithin~25 = 0 (not contained) - If the point lies on an edge:
!
isOnEdge~25 = 1 (on an edge)
!
isWithin~25 = 0 (not on an edge)
!Calculation: isWithin~25 = 0 isOnEdge~25 = 0 j~25 = 0 repeat j~25 = j~25 + 1 nxt~25 = j~25%pgonN[i~25] + 1 !Start and End Points of the Edge x1~25 = pgonX[i~25][j~25] y1~25 = pgonY[i~25][j~25] x2~25 = pgonX[i~25][nxt~25] y2~25 = pgonY[i~25][nxt~25] !Check if the point falls on the edge if abs(x2~25 - x1~25) > tol then
Figure 328 – We should also check whether the point lies on an edge of the polygon.
Chapter 21: Polygon Operations - Check if a Point Lies Inside a Polygon 371 !Non-vertical Edge if xi~25 > min(x1~25, x2~25) and xi~25 < max(x1~25, x2~25) then yTest~25 = y1~25 + (xi~25 - x1~25)*(y2~25 - y1~25)/(x2~25 - x1~25) if abs(yTest~25 - yi~25) < tol then isWithin~25 = 0 isOnEdge~25 = 1 return endif endif else !Vertical Edge if abs(x1~25 - xi~25) < tol then if yi~25 > min(y1~25, y2~25) - tol and yi~25 < max(y1~25,y2~25) + tol then isWithin~25 = 0 isOnEdge~25 = 1 return endif endif endif !Check how the edge contributes to isWithin addWithin~25 = 0 !If the point falls directly below a vertex, contribute a half unit if (abs(x1~25 - xi~25) < tol and y1~25 > yi~25 + tol) then addWithin~25 = sgn(x1~25 - x2~25) endif if (abs(x2~25 - xi~25) < tol and y2~25 > yi~25 + tol) then addWithin~25 = sgn(x1~25 - x2~25) endif !If the point falls somewhere below an edge, contribute 2 units if not(addWithin~25) then if x2~25 < x1~25 then if xi~25 > x2~25 and xi~25 < x1~25 then yTest~25 = y1~25 + (xi~25 - x1~25)*(y2~25 - y1~25)/(x2~25 - x1~25) if yTest~25 > yi~25 + tol then addWithin~25 = 2 endif
372 Chapter 21: Polygon Operations - Polygon Addition endif else if xi~25 > x1~25 and xi~25 < x2~25 then yTest~25 = y1~25 + (xi~25 - x1~25)*(y2~25 y1~25)/(x2~25 - x1~25) if yTest~25 > yi~25 + tol then addWithin~25 = -2 endif endif endif endif
Figure 329 – Our aim is to add the two polygons .
!Add the contribution isWithin~25 = isWithin~25 + addWithin~25 until j~25 >= pgonN[i~25] !Set isWithin~25 to 1 if it is positive isWithin~25 = sgn(isWithin~25) return
Notice that in this sub-routine we add ±2 for an edge contribution and ±1 for a vertex contribution. This is different to what we discussed. We said we would add ±1 for an edge contribution and ±½ for a vertex contribution. The change was made deliberately so that the isWithin~25 and addWithin~25 variables would remain integers, thus avoiding any round-off errors.
Figure 330 – The first step is to split all edges of polygon A where they intersect with edges of polygon B, and vice versa.
21.5 Polygon Addition To add two polygons A and B together (figure 329), the approach is first to split all edges of polygon A against all edges of polygon B (figure 330). We then remove any edge belonging to B that lies inside A (figure 331) and vice versa. Finally we will use the remaining edges to construct a new set of polygons. First, then, we will split the edges (figure 330). This process will make use of the 6 Intersection of Two Line Segments - 2D subroutine. Each time we find an intersection point that splits the two line segments, we’ll insert it as the start point of a new edge in Figure 331 – Edges of B that lie the polygon. The number of vertices in the polygon will increase, so we need to inside A are removed. constantly revise the end condition of the loop – hence we use a repeat – until rather than a for – next loop.
Chapter 21: Polygon Operations - Polygon Addition 373 Having split the polygons, we mark each edge of polygon B as excluded if it lies inside polygon A (figure 331) and vice versa (figure 332) To check if an edge lies within a polygon, we can test its midpoint using the 25 Polygon Contains Point sub-routine. The reason that we check the midpoint of the edge rather than one of its end-points, is to avoid round-off error. We can be certain that the midpoint is at least tol away from any other point. If it were closer than tol, the edge Figure 332 – Similarly, edges of A would have already been split as part of the previous step. Finally, the resulting polygon is assembled by piecing together the remaining edges until we have completed a closed shape (figure 333). We can start at any edge that has not been excluded. The next edge will be either the next edge of this polygon, or, if the next edge has been excluded, an edge of the other polygon that shares the same start point.
that lie inside B are removed.
To see this working, create a new object and copy the following GDL script into its 2D Script editing window. tol = 0.00001 !Original Polygons to Merge dim origPgonX[][], origPgonY[][], origPgonN[] put 0, 0, .7, 0, 1, 1, 0, 1 origPgonN[1] = nsp/2 for j = 1 to 2*origPgonN[1] origPgonX[1][j] = get(1) origPgonY[1][j] = get(1) next j put 0, -.2, 1, -.2, .3, .3, 1, .5, .3, .6, 1, .7, 0, .7 origPgonN[2] = nsp/2 for j = 1 to 2*origPgonN[2]
Figure 333 – Finally, the remaining edges are stitched together to form a new polygon.
374 Chapter 21: Polygon Operations - Polygon Addition origPgonX[2][j] = get(1) origPgonY[2][j] = get(1) next j !Draw the Original Polygons for i = 1 to 2 pen i for j = 1 to origPgonN[i] put origPgonX[i][j], origPgonY[i][j] next j poly2 nsp/2, 5, get(nsp) next j
Click on the 2D Full View button to run the 2D Script. You’ll see the shapes of the original polygons (figure 334). Now add the following script, and the necessary sub-routines listed below. Comment out the script block that draws the original polygons. Finally click on the 2D Full View button to run the script. You will see that the resulting polygons accurately trace the shape of the merged polygons. At the head of the script, declare the global arrays for the working polygons and the resulting polygons from the operations. These arrays need only be declared once. !Declare arrays for working polygons and results dim pgonN[], pgonX[][],
Figure 334 – Run the 2D script to view the original polygons.
pgonY[][], includeEdge[][] dim resultN[], resultX[][], resultY[][]
Now at the end of the script we’ll add the block of script that calls the sub-routine. !Set up working polygons to match the original polygons and send to Add Polygons sub-routine for i = 1 to 2 pgonN[i] = origPgonN[i] pgonX[i] = origPgonX[i] pgonY[i] = origPgonY[i] next i
Chapter 21: Polygon Operations - Polygon Addition 375 i1~26 = 1 i2~26 = 2 gosub "26 Add Polygons"
Finally we’ll add some script to draw the returned resulting polygons (figure 335). !Draw the resulting polygons pen 2 for i = 1 to nResults for j = 1 to resultN[i] nxt = j%resultN[i] + 1 line2 resultX[i][j], resultY[i][j], resultX[i][nxt], resultY[i][nxt] next j next i end
The sub-routine 26 Add Polygons is given below. Note that polygon addition, subtraction and intersection sub-routines all use the 27 Split Edges, 6 Intersection of Two Line Segments - 2D, 25 Polygon Contains Point and 28 Reconstruct Polygons sub-routines which are also given here. "26 Add Polygons": !Input: ! !
- global working polygons pgonN[], pgonX[][], pgonY[][] (we will always be adding the first two polygons in the array
!
but a variation of the sub-routine could be created that will
!
add any two polygons i1~26 and i2~26)
!Required Sub-routines: !
- “25 Polygon Contains Point”
!
- “27 Split Edges”
!
- “28 Reconstruct Polygons”
!Return: ! !
- global result polygons nResults, resultN[], resultX[][], resultY[][] nResults = 0
!Calculation: !Split the edges of polygons i1~26 and i2~26 gosub "27 Split Edges"
Figure 335 – Run the 2D script to see the resulting polygon.
376 Chapter 21: Polygon Operations - Polygon Addition !Remove any edge in polygon i1~26 that lies in polygon i2~26 i~25 = i2~26 for j~26 = 1 to pgonN[i1~26] nxt~26 = j~26%pgonN[i1~26] + 1 xi~25 = (pgonX[i1~26][j~26] + pgonX[i1~26][nxt~26])/2 yi~25 = (pgonY[i1~26][j~26] + pgonY[i1~26][nxt~26])/2 gosub "25 Polygon Contains Point" if isWithin~25 then includeEdge[i1~26][j~26] = 0 else includeEdge[i1~26][j~26] = 1 endif next j~26 !Remove any edge in polygon i2~26 that lies strictly within polygon i1~26 i~25 = i1~26 for j~26 = 1 to pgonN[i2~26] nxt~26 = j~26%pgonN[i2~26] + 1 xi~25 = (pgonX[i2~26][j~26] + pgonX[i2~26][nxt~26])/2 yi~25 = (pgonY[i2~26][j~26] + pgonY[i2~26][nxt~26])/2 gosub "25 Polygon Contains Point" if isOnEdge~25 or isWithin~25 then includeEdge[i2~26][j~26] = 0 else includeEdge[i2~26][j~26] = 1 endif next j~26 !Reconstruct the new polygons and return the result in result[][] i1~28 = i1~26 i2~28 = i2~26 gosub "28 Reconstruct Polygons" return
As mentioned above, the 26Add Polygons sub-routine makes use of a bunch of new sub-routines. These are listed below. "27 Split Edges": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- indices of the polygons to split i1~27 and i2~27
Chapter 21: Polygon Operations - Polygon Addition 377 !Required Sub-routines: !
- “6 Intersection of Two Line Segments - 2D”
!Return: !
- global working polygons
!Calculation: i~27 = 0 repeat !First edge to test for splitting i~27 = i~27 + 1 nxti~27 = i~27%pgonN[i1~27] + 1 x11~6 = pgonX[i1~27][i~27] y11~6 = pgonY[i1~27][i~27] x12~6 = pgonX[i1~27][nxti~27] y12~6 = pgonY[i1~27][nxti~27] !Compare with all edges of the other polygon j~27 = 0 repeat !Second edge to test for splitting j~27 = j~27 + 1 nxtj~27 = j~27%pgonN[i2~27] + 1 x21~6 = pgonX[i2~27][j~27] y21~6 = pgonY[i2~27][j~27] x22~6 = pgonX[i2~27][nxtj~27] y22~6 = pgonY[i2~27][nxtj~27] gosub “6 Intersection of Two Line Segments - 2D” !If the first line segment splits, add an edge to polygon 1 if lineSplits1~6 then !Make a space for the new edge pgonN[i1~27] = pgonN[i1~27] + 1 for k~27 = pgonN[i1~27] to i~27 + 1 step -1 pgonX[i1~27][k~27] = pgonX[i1~27][k~27 - 1] pgonY[i1~27][k~27] = pgonY[i1~27][k~27 - 1] next k~27 !Set the start point of the new edge pgonX[i1~27][i~27 + 1] = x~6 pgonY[i1~27][i~27 + 1] = y~6
378 Chapter 21: Polygon Operations - Polygon Addition !Adjust the end point of the first line x12~6 = x~6 y12~6 = y~6 endif !If the second line segment splits, add an edge to polygon 2 if lineSplits2~6 then !Make a space for the new edge pgonN[i2~27] = pgonN[i2~27] + 1 for k~27 = pgonN[i2~27] to j~27 + 1 step -1 pgonX[i2~27][k~27] = pgonX[i2~27][k~27 - 1] pgonY[i2~27][k~27] = pgonY[i2~27][k~27 - 1] next k~27 !Set the start point of the new edge pgonX[i2~27][j~27 + 1] = x~6 pgonY[i2~27][j~27 + 1] = y~6 endif until j~27 = pgonN[i2~27] until i~27 = pgonN[i1~27] return
In the 27 Split Edges sub-routine, the slowest part is where we add an edge to the edge list. All the edges above shuffle up one index, which can mean shifting a lot of data around if the polygons are large. If you have time you might want to look into how this sub-routine could be optimized by avoiding this data shuffling. However it works quite well as is. The next two sub-routines we have seen before. "6 Intersection of Two Line Segments - 2D": (this sub-routine is given in chapter 18 Linear Algebra Applications) return "25 Polygon Contains Point": (see the previous section for this sub-routine) return
The 28 Reconstruct Polygons sub-routine listed below is used by a bunch of the polygon operations. "28 Reconstruct Polygons": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- indices of the polygons to reconstruct i1~28, i2~28
!
- global edge inclusion includeEdge[][]
Chapter 21: Polygon Operations - Polygon Addition 379 !Return: ! !
- Recombined edges to form a set of new polygons nResults, resultN[], resultX[][], resultY[][] nResults = 0
!Calculation: repeat !Find a start point startPoint~28 = 0 thisPolygon~28 = 0 repeat if thisPolygon~28 = 0 then thisPolygon~28 = i1~28 otherPolygon~28 = i2~28 else thisPolygon~28 = i2~28 otherPolygon~28 = i1~28 endif thisVertex~28 = 0 repeat thisVertex~28 = thisVertex~28 + 1 startPoint~28 = includeEdge[thisPolygon~28][thisVertex~28] until startPoint~28 or thisVertex~28 = pgonN[thisPolygon~28] until startPoint~28 or thisPolygon~28 = i2~28 if not(startPoint~28) then return endif !Start a new polygon nResults = nResults + 1 resultN[nResults] = 1 resultX[nResults][1] = pgonX[thisPolygon~28][thisVertex~28] resultY[nResults][2] = pgonY[thisPolygon~28][thisVertex~28] includeEdge[thisPolygon~28][thisVertex~28] = 0 repeat edgeFound~28 = 0 nextVertex~28 = thisVertex~28%pgonN[thisPolygon~28] + 1 !Check the next edge of this polygon
380 Chapter 21: Polygon Operations - Polygon Addition if includeEdge[thisPolygon~28][nextVertex~28] then thisVertex~28 = nextVertex~28 resultN[nResults] = resultN[nResults] + 1 resultX[nResults][resultN[nResults]] = pgonX[thisPolygon~28][thisVertex~28] resultY[nResults][resultN[nResults]] = pgonY[thisPolygon~28][thisVertex~28] includeEdge[thisPolygon~28][thisVertex~28] = 0 edgeFound~28 = 1 goto "28 Continue" endif !Otherwise, find an edge on the other polygon !that starts at the end of the current edge !Find the start point of the next edge nextStartX~28 = pgonX[thisPolygon~28][nextVertex~28] nextStartY~28 = pgonY[thisPolygon~28][nextVertex~28] !Search for an adjacent edge j~28 = 0 repeat j~28 = j~28 + 1 if includeEdge[otherPolygon~28][j~28] then if abs(pgonX[otherPolygon~28][j~28] - nextStartX~28) < tol then if abs(pgonY[otherPolygon~28][j~28] - nextStartY~28) < tol then edgeFound~28 = 1 endif endif endif until edgeFound~28 or j~28 = pgonN[otherPolygon~28] !Add the edge to the resulting polygon if edgeFound~28 then thisPolygon~28 = otherPolygon~28 otherPolygon~28 = thisPolygon~28%2 + 1 thisVertex~28 = j~28 resultN[nResults] = resultN[nResults] + 1 resultX[nResults][resultN[nResults]] = pgonX[thisPolygon~28][thisVertex~28] resultY[nResults][resultN[nResults]] = pgonY[thisPolygon~28][thisVertex~28] includeEdge[thisPolygon~28][thisVertex~28] = 0 edgeFound~28 = 1
Chapter 21: Polygon Operations - Intersection of Two Polygons 381 goto "28 Continue" endif "28 Continue": until not(edgeFound~28) until not(startPoint~28) return
Note that this sub-routine constructs any number of new polygons, based on which edges are marked as included. It is up to the polygon operation sub-routine (in this case 26 Add Polygons) to decide which edges should be included. Figure 336 – To find the Click on the 2D Full View button to run the script (figure 335). Try different values for intersection of two polygons, we start by splitting the edges as we did the original polygons. for polygon addition.
21.6 Intersection of Two Polygons The process of finding the intersection of polygons A and B, is similar to that of merging the polygons. We split the edges (figure 336), choose which to include, and recombine them to form polygons. The main difference is choosing which edges to include in the resulting polygon. To Figure 337 – In this case we keep achieve an intersection, we mark each edge of polygon A as included if it lies loosely only edges of polygon B that lie inside of polygon B (figure 337). We mark each edge of polygon B as included if it lies within polygon A and vice versa. strictly inside of polygon A. Re-combining the edges to form polygons yields the intersection of the two original polygons (figure 338). Make a copy of your 26 Add Polygons sub-routine, and name it 29 Intersect Polygons. Edit the script as shown below. "29 Intersect Polygons": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- the indices of the two polygons i1~29 and i2~29
!Required Sub-routines: !
- “25 Polygon Contains Point”
!
- “27 Split Edges”
Figure 338 – Finally we use the remaining edges to construct one or more new polygons.
382 Chapter 21: Polygon Operations - Intersection of Two Polygons !
- “28 Reconstruct Polygons”
!Return: !
- global result polygons nResults,
!
resultN[], resultX[][], resultY[][] nResults = 0
!Calculation: !Split the edges of polygons 1 and 2 i1~27 = i1~29 i2~27 = i2~29 gosub "27 Split Edges" !Remove any edge in pgon 1 that lies strictly outside pgon 2 i~25 = i2~29 for j~29 = 1 to pgonN[i1~29] nxt~29 = j~29%pgonN[i1~29] + 1
Figure 339 – Run the 2D script to view the original polygons (lines only) and the intersection (filled polygons).
xi~25 = (pgonX[i1~29][j~29] + pgonX[i1~29][nxt~29])/2 yi~25 = (pgonY[i1~29][j~29] + pgonY[i1~29][nxt~29])/2 gosub "25 Polygon Contains Point" if isWithin~25 or isOnEdge~25 then includeEdge[i1~29][j~29] = 1 else includeEdge[i1~29][j~29] = 0 endif next j~29 !Remove any edge in pgon 2 that lies strictly within pgon 1
Figure 340 – As before, the edges of both polygons are split.
pg~25 = i1~29 for j~29 = 1 to pgonN[i2~29] nxt~29 = j~29%pgonN[i2~29] + 1 xi~25 = (pgonX[i2~29][j~29] + pgonX[i2~29][nxt~29])/2 yi~25 = (pgonY[i2~29][j~29] + pgonY[i2~29][nxt~29])/2 gosub "25 Polygon Contains Point" if isWithin~25 then includeEdge[i2~29][j~29] = 1 else includeEdge[i2~29][j~29] = 0 endif next j~29
Figure 341 – Edges of A are removed if they lie inside B.
Chapter 21: Polygon Operations - Subtract One Polygon from Another 383 !Reconstruct the new pgons and return the result in result[][] i1~28 = i1~29 i2~28 = i2~29 gosub "28 Reconstruct Polygons" return
Click the 2D Full View button to run the script (figure 339). Notice that multiple Figure 342 – Edges of B are reresult polygons may be generated. versed.
21.7 Subtract One Polygon from Another Subtracting polygon B from polygon A is nearly identical to adding the two, but there are some differences. As before, we first split all of the edges (figure 340). We then discard any edge of polygon A that lies inside of polygon B (figure 341). At this point, we reverse the order of points in polygon B (figure 342). This is because, Figure 343 – Edges of B that lie in the new polygon set, the edges will run in the opposite direction. Having reversed the edge order, we can discard any edge of polygon B that lies outside of polygon A (figure 343)
outside of A are removed.
Finally we re-combine the edges as before (figure 344). Try it Yourself: Subtract One Polygon from Another Copy the 26 Add Polygons sub-routine, and re-name it 30 Subtract Polygons. Change the sub-routine to match the following script (figure 345). "30 Subtract Polygons": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
Figure 344 – Remaining edges are re-combined to form polygons.
! - polygon indices i1~30, i2~30 (where i2~30 will be subtracted from i1~30) !Required Sub-routines: !
- “25 Polygon Contains Point”
!
- “27 Split Edges”
!
- “28 Reconstruct Polygons”
!Return: !
- global result polygons nResults, resultN[], resultX[][], resultY[][]
384 Chapter 21: Polygon Operations - Subtract One Polygon from Another nResults = 0 !Calculation: !Split the edges i1~27 = i1~30 i2~27 = i2~30 gosub "27 Split Edges" !Remove any edge in polygon i1~30 !that lies strictly within polygon i2~30 i~25 = i2~30 for j~30 = 1 to pgonN[i1~30] nxt~30 = j~30%pgonN[i1~30] + 1
Figure 345 – Run the 2D script to view the original polygons as lines and the results as fill polygons.
xi~25 = (pgonX[i1~30][j~30] + pgonX[i1~30][nxt~30])/2 yi~25 = (pgonY[i1~30][j~30] + pgonY[i1~30][nxt~30])/2 gosub "25 Polygon Contains Point" if isWithin~25 then includeEdge[i1~30][j~30] = 0 else includeEdge[i1~30][j~30] = 1 endif next j~30 !Reverse the edge order of the second polygon dim tempX~30[], tempY~30[] tempX~30 = pgonX[i2~30] tempY~30 = pgonY[i2~30] for j~30 = 1 to pgonN[i2~30] k~30 = pgonN[i2~30] + 1 - j~30 pgonX[i2~30][j~30] = tempX~30[k~30] pgonY[i2~30][j~30] = tempY~30[k~30] next j~30 !Remove any edge in pgon i2~30 that lies outside pgon i1~30 i~25 = i1~30 for j~30 = 1 to pgonN[i2~30] nxt~30 = j~30%pgonN[i2~30] + 1 xi~25 = (pgonX[i2~30][j~30] + pgonX[i2~30][nxt~30])/2 yi~25 = (pgonY[i2~30][j~30] + pgonY[i2~30][nxt~30])/2 gosub "25 Polygon Contains Point"
Figure 346 - All the edges are offset.
Chapter 21: Polygon Operations - Offset a Polygon 385 if isWithin~25 or isOnEdge04 then includeEdge[i2~30][j~30] = 1 else includeEdge[i2~30][j~30] = 0 endif next j~30 !Reconstruct the new polygons and return result[][] gosub "28 Reconstruct Polygons" return
Figure 347 - The intersection points are calculated for each pair of offset edges.
21.8 Offset a Polygon Here we’ll develop a sub-routine that will allow us to offset each edge of a polygon by a different amount. This process is somewhat different to the preceding ones, as we are dealing with only one polygon. First we’ll offset the edges (figure 346). Next we’ll re-calculate the intersection points to find the new vertices (figure 347). Then we’ll split each edge across all other edges in the polygon (figure 348).
Figure 348 - All edges are re-combined to form new polygons.
We re-combine the edges to form new polygons (figure 349). Any resulting polygons with negative area are discarded (figure 350). There is one remaining problem as shown in the illustration. In some cases the process described above results in two or more polygons that are superimposed (figure 350). To remove such cases we must extend the sub-routine to remove any polygon of the result set whose edges lie inside another polygon of the result set. "31 Offset Polygon": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- polygon index i~31
!Required Sub-routines: !
- “32 Split Own Edges”
!Return:
Figure 349 – Edges re-combined to form polygons.
386 Chapter 21: Polygon Operations - Offset a Polygon ! - global result polygons nResults, resultN[], resultX[][], resultY[][] nResults = 0 !Calculation: !Edge direction vectors dim ux~31[], uy~31[] for j~31 = 1 to pgonN[i~31] nxt~31 = j~31%pgonN[i~31] + 1 !Direction Vector
Figure 350 – Polygons that have negative area are removed.
ux~31[j~31] = pgonX[i~31][nxt~31] - pgonX[i~31][j~31] uy~31[j~31] = pgonY[i~31][nxt~31] - pgonY[i~31][j~31] !Normalize L~31 = sqr(ux~31[j~31]^2 + uy~31[j~31]^2) ux~31[j~31] = ux~31[j~31]/L~31 uy~31[j~31] = uy~31[j~31]/L~31 next j~31 !Offset the Polygon Edges dim tempX~31[], tempY~31[] for j~31 = 1 to pgonN[i~31] nxt~31 = j~31%pgonN[i~31] + 1 !Offset tempX~31[j~31] = pgonX[i~31][j~31] + uy~31[j~31]*workingOffset[j~31] tempY~31[j~31] = pgonY[i~31][j~31] ux~31[j~31]*workingOffset[j~31] next j~31 !Intersect adjacent edges to form a new offset polygon for j~31 = 1 to pgonN[i~31] if j~31 = 1 then prv~31 = pgonN[i~31] else prv~31 = j~31 - 1 endif x11~6 = tempX~31[prv~31] y11~6 = tempY~31[prv~31] x12~6 = x11~6 + ux~31[prv~31]
Figure 351 - In some cases an offset may result in superimposed polygons.
Chapter 21: Polygon Operations - Offset a Polygon 387 y12~6 = y11~6 + uy~31[prv~31] x21~6 = tempX~31[j~31] y21~6 = tempY~31[j~31] x22~6 = x21~6 + ux~31[j~31] y22~6 = y21~6 + uy~31[j~31] gosub “6 Intersection of Two Line Segments - 2D” if parallel~6 then pgonX[i~31][j~31] = tempX~31[j~31] pgonY[i~31][j~31] = tempY~31[j~31] else pgonX[i~31][j~31] = x~6 pgonY[i~31][j~31] = y~6 endif next j~31 !Split edges on each other i~32 = i~31 gosub "32 Split Own Edges" !Repeatedly re-combine edges to form polygons for j~31 = 1 to pgonN[i~31] includeEdge[i~31][j~31] = 1 next j~31 repeat !Find the next available start point startFound~31 = 0 j~31 = 0 repeat j~31 = j~31 + 1 until j~31 >= pgonN[i~31] or includeEdge[i~31][j~31] if includeEdge[i~31][j~31] then startFound~31 = 1 !Start a new result polygon nResults = nResults + 1 resultN[nResults] = 1 resultX[nResults][1] = pgonX[i~31][j~31] resultY[nResults][2] = pgonY[i~31][j~31] includeEdge[1][j~31] = 0
388 Chapter 21: Polygon Operations - Offset a Polygon nxt~31 = j~31%pgonN[i~31] + 1 nextStartX~31 = pgonX[i~31][nxt~31] nextStartY~31 = pgonY[i~31][nxt~31] !Scan for the last edge intersecting with this edge repeat adjacentEdgeFound~31 = 0 k~31 = j~31 repeat if k~31 = 1 then k~31 = pgonN[i~31] else k~31 = k~31 - 1 endif if includeEdge[i~31][k~31] then if abs(pgonX[i~31][k~31] - nextStartX~31) < tol then if abs(pgonY[i~31][k~31] - nextStartY~31) < tol then adjacentEdgeFound~31 = 1 resultN[nResults] = resultN[nResults] + 1 resultX[nResults][resultN[nResults]] = pgonX[i~31][k~31] resultY[nResults][resultN[nResults]] = pgonY[i~31][k~31] includeEdge[i~31][k~31] = 0 nxt~31 = k~31%pgonN[i~31] + 1 nextStartX~31 = pgonX[i~31][nxt~31] nextStartY~31 = pgonY[i~31][nxt~31] endif endif endif until k~31 = j~31 or adjacentEdgeFound~31 until not(adjacentEdgeFound~31) !Remove the polygon if it has a negative area area~31 = 0 for j~31 = 1 to resultN[nResults] nxt~31 = j~31%resultN[nResults] + 1 area~31 = area~31 - (resultX[nResults][nxt~31] resultX[nResults][j~31])*(resultY[nResults][nxt~31] + resultY[nResults][j~31])/2 next j~31 if area~31 < 0 then
Chapter 21: Polygon Operations - Offset a Polygon 389 nResults = nResults - 1 endif endif until not(startFound~31) return
The 32 Split Own Edges sub-routine is given below. "32 Split Own Edges": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- polygon index i~32
!Required Sub-routines: !
- “32 Split Own Edges”
!Return: !
- global working polygons pgonN[], pgonX[][], pgonY[][] nResults = 0
!Calculation: j~32 = 0 repeat j~32 = j~32 + 1 nxtj~32 = j~32%pgonN[i~32] + 1 x11~6 = pgonX[i~32][j~32] y11~6 = pgonY[i~32][j~32] x12~6 = pgonX[i~32][nxtj~32] y12~6 = pgonY[i~32][nxtj~32] k~32 = 0 repeat k~32 = k~32 + 1 if k~32 # j~32 then nxtk~32 = k~32%pgonN[i~32] + 1 x21~6 = pgonX[i~32][k~32] y21~6 = pgonY[i~32][k~32] x22~6 = pgonX[i~32][nxtk~32] y22~6 = pgonY[i~32][nxtk~32] gosub “6 Intersection of Two Line Segments - 2D” !If the first line segment splits, add an edge to polygon 1 if lineSplits1~6 then
390 Chapter 21: Polygon Operations - Closest Point on a Polygon !Make a space for the new edge pgonN[i~32] = pgonN[i~32] + 1 for h~32 = pgonN[i~32] to j~32 + 1 step -1 pgonX[i~32][h~32] = pgonX[i~32][h~32 - 1] pgonY[i~32][h~32] = pgonY[i~32][h~32 - 1] next h~32 !Set the start point of the new edge pgonX[i~32][j~32 + 1] = x~6 pgonY[i~32][j~32 + 1] = y~6 !Adjust the end point of the first line x12~6 = x~6 y12~6 = y~6 endif endif until k~32 >= pgonN[i~32] until j~32 = pgonN[i~32] return
21.9 Closest Point on a Polygon This is a natural extension of the problem discussed in the section Closest Point on a Line Segment in chapter 18 Linear Algebra Applications. In this case, however, there are multiple line segments – one for each edge of the polygon. The closest point on the polygon’s perimeter could lie on any one of these edges. We need to test each edge to find the closest point. "33 Closest Point on a Polygon": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- the index of the polygon to check = i~33
!
- a point (x~33, y~33) if getValues then i~33 = get(1) x~33 = get(1): y~33 = get(1) getValues = 0 endif
!Return: !
- co-ordinates of the closest point on the polygon’s perimeter
Chapter 21: Polygon Operations - Center of Gravity 391 !
(closestX~33, closestY~33)
!
- distance of the point from the polygon = closestD~33
!
- which edge of the polygon is closest = closestEdge~33
!Calculation closestX~33 = pgonX[i~33][1] closestY~33 = pgonY[i~33][1] closestD~33 = sqr((closestX~33 – x~33)^2 + (closestY~33 – y~33)^2) for j~33 = 1 to pgonN[i~33] nxt~33 = j~33%pgonN[i~33] + 1 x1~34 = pgonX[i~33][j~33] y1~34 = pgonY[i~33][j~33] x2~34 = pgonX[i~33][nxt~33] y2~34 = pgonY[i~33][nxt~33] xi~34 = x~33 yi~34 = y~33 gosub "34 Closest Point on a Line Segment – 2D" D~33 = sqr((x1~34 – x~33)^2 + (y1~34 – y~33)^2) if D~33 < closestD~33 then closestX~33 = x1~34 closestY~33 = y1~34 closestD~33 = D~33 closestEdge~33 = j~33 endif next j~33 return
21.10
Center of Gravity
It can be handy to calculate a polygon’s center of gravity or centroid. Visually the centroid marks a natural reference point for a polygon. If you wanted to label the polygon, the centroid would be a good choice of origin for the label.
21.10.1 Centroid of Masses on a Balance Beam Imagine a straight beam along which hang objects of mass mi , each at a position xi . We’ll assume the beam itself is weightless. Each mass contributes a torque mi xi around the origin. Our concept of center of gravity or centroid is that, at some point X along the beam, there is a point where you could
392 Chapter 21: Polygon Operations - Center of Gravity place a fulcrum, so that the beam is balanced. At this point, all of the individual masses on the beam can be balanced by an upward force equivalent to that exerted by a mass M (equal to the sum of the individual masses). To find the centroid X , then, we can simply balance the contributed by the individual masses with that of the torque provided by the single upward force exerted by the fulcrum.
MX =
∑m x
i i
Our problem reduces to solving the equation below.
X =
∑m x ∑m i
i
i
We will see that the centroid of a polygon can be calculated along any axis by ‘balancing the torques’ of all the triangles that make up the polygon.
21.10.2 Centroid of a Triangle It turns out that the centroid of a triangle is also easy to calculate. Run lines from each vertex to the midpoint of the opposite side. The lines all meet at a single point - the centroid of the triangle (figure 352). If you do the math, you’ll find that this point is precisely two-thirds the distance between the vertex and the opposite side. Figure 352 – The centroid of a triangle can be found by running lines from each vertex to When we performed our area calculation earlier in the chapter, we the bisector of the opposite side.
21.10.3 Centroid of a Polygon
reconstructed our polygon by adding a bunch of triangles that had positive and negative areas. Each triangle had one vertex at the origin, and the opposite edge was formed by an edge of the polygon. In the same way we can add the centroids of the triangles to get the centroid of the polygon. A triangle doesn’t really have a mass, but we can imagine that our polygon has a uniform ‘area density’ as if it were cut from a piece of sheet metal. A triangle with negative area has negative mass. Our equation for the center of gravity can be applied independently along any axis. We will apply it along the x-axis to find the x-ordinate of the center of gravity of our polygon.
X =
∑m x ∑m i
i
i
Chapter 21: Polygon Operations - Center of Gravity 393 We can re-write our equation, substituting area for mass.
X =
∑a x i
i
A Clearly the centroid of each triangle lies two-thirds of the distance between the origin and the polygon edge mid-point. We also know that the area is equal to half the cross-product of the other two edges (i.e. the polygon edge completing the triangle). Thus the contribution of our first edge is: 1 (x y − x y )⋅ 2 (x + x ) i i +1 i +1 i i i +1 2 3 = 2
ai xi
(x y i
i +1
− xi +1 yi )( xi + xi +1 ) 6
If we sum the contributions of all the triangles, we get the centroid.
X = 1
∑(x y − x 3 ∑(x y i
i +1
i +1
i +1
i
yi )( xi + xi +1 ) − xi +1 yi )
By symmetry, the y-ordinate for the centroid has the same form.
Y = 1
∑(x y − x 3 ∑(x y i
i +1
i
i +1
yi )( yi + yi +1 )
i +1
− xi +1 yi )
The following sub-routine calculates the area and centroid of a polygon simultaneously. "39 Centroid and Area": !Input: !
- global working polygons pgonN[], pgonX[][], pgonY[][]
!
- the index of the polygon to check = i~39
!Return: !
- the area area~39
!
- the co-ordinates of the centroid (cX~39, cY~39)
!Calculation: area~39 = 0 cX~39 = 0 cY~39 = 0 for j~39 = 1 to pgonN[i~39] nxt~39 = j~39%pgonN[i~39] + 1 areaContribution~39 = pgonX[i~39][j~39]*pgonY[i~39][nxt~39] pgonX[i~39][nxt~39]*pgonY[i~39][j~39]
394 Chapter 21: Polygon Operations - Optimizing the Polygon Operations cX~39 = cX~39 + areaContribution~39*(pgonX[i~39][j~39] + pgonX[i~39][nxt~39]) cY~39 = cY~39 + areaContribution~39*(pgonY[i~39][j~39] + pgonY[i~39][nxt~39]) area~39 = area~39 + areaContribution~39 next j~39 cX~39 = cX~39/(3*area~39) cY~39 = cY~39/(3*area~39) area~39 = area~39/2 return
21.11
Optimizing the Polygon Operations
Many of the polygon operations discussed in this chapter use the 27 Split Edges, 25 Polygon Contains Point and 28 Reconstruct Polygons sub-routines. These sub-routines tend to be slow, especially the 27 Split Edges, as it checks every edge in one polygon against every edge in the other polygon. For polygons with few edges this is no problem, but for more complex shapes the number of calculations increases dramatically. 27 Split Edges also has the unfortunate habit of repeatedly shuffling data each time an edge is split. To make the polygon operation sub-routines more efficient, we can try a couple of techniques. We will try to reduce the amount of work 27 Split Edges does by making it first compare the bounding rectangles of the polygons. We will then use an array of indices to store the order of edges to avoid shuffling data. We will also discuss how to reduce the number of edges in our polygons by allowing edges to be either straight, or circular arcs.
21.11.1 Avoid Splitting Edges When we are dealing with polygons containing large numbers of edges, 27 Split Edges can slow down. We can improve the speed of the operation by calling the sub-routine only when needed, or at least identifying situations where we can quickly see it will not be required. In particular, we know that no edges will split if the bounding rectangles of the two polygons do not intersect. The plan is to calculate the bounding rectangles, then check for an overlap of these rectangles. If no such overlap occurs, we know that there is no intersection of any of the edges, so do not need to continue with the 27 Split Edges sub-routine.
21.11.2 Avoid Shuffling Data Each time data is shuffled to make space for a new point, a lot of processing is done. To avoid this processing we can create an array of edge indices, that keeps track of the order of the data. Rather than
Chapter 21: Polygon Operations - Optimizing the Polygon Operations 395 shuffling the data itself, new data is added to the end of the arrays, and the indices are updated. For example, imagine a four-sided polygon. The initial edge order indices are (1, 2, 3, 4). Let’s say that edge 2 is split. We add the start point of the new edge in the fifth position. The index list becomes (1, 2, 5, 3, 4). This involves shuffling only one array, rather than two arrays (x, y) or possibly more if more data is stored about each edge. Once all splitting is complete, the data can be re-assigned to the correct indices in a single process.
21.11.3 Extended Edge Types We can extend our polygon definition by allowing the inclusion of curved (circular arc) edges. To define curved edges, each edge is assigned an angle. A zero angle is interpreted as a straight edge. A positive angle means a convex edge, and a negative angle means a concave edge. This will result in fewer edges in the polygon, as a single curved edge was previously approximated by multiple short, straight edges. Fewer edges means faster processing. On the down side, the algorithms become more complex. We must revise all of the polygon sub-routines to allow for curved edges. The Polygon Area sub-routine must add or subtract the areas inside the arcs. The Split Edges sub-routine needs modification when calculating the bounding rectangles and when splitting edges.
22 Quality Control Quality is key. However good your ideas are, if your software is unreliable it will frustrate and annoy the end users. This will translate to bad publicity and fewer sales. Every application that you create must be incredibly robust if it is to be practically useful. Within its scope it must work well every time. If your software causes ArchiCAD to crash, for example, it will become inefficient for people to use it. This demand for robust software is a real challenge for developers. It is increased if you are designing for a mass market, as each end-user will use the application in a slightly different way, and in a slightly different context. In this chapter we’ll look at how you can build reliability into your software from the start, and how to efficiently identify and fix any remaining issues.
22.1 Scripting Style At the scripting stage, quality can be built directly into your software. Taking the necssary time at this stage can save hours of wasted effort in the future, by reducing the amount of testing and bug fixing required, and by making bug fixing easier.
22.1.1
Use a Clear Scripting Style
The easier it is to read your script, the less likely it will be for you to make an error, and the easier it will be to find and fix an error should one occur. It is often tempting to script concisely at the expense of readability. Nesting operations is a typical example. If you do this, use a comment to explain what you have done and why. There may occasionally be good reasons for writing a more complex line of code. When this happens, the reason for introducing complexity should be important and clearly defined. Add a comment in your code to clearly explain why the complexity has been introduced. If you can’t be bothered with a comment, or if you can’t clearly explain why you decided to write a convoluted line of script where three simpler lines would have done the same job, then don’t. Go with the simple script instead. Err on the side of readability every time. For instance, you could perform a series of transformations in a single step by calculating the matrix product of the individual transformations. This approach may result in better overall performance. If this is the case, then by all means use the single transformation matrix. But be sure to include a comment that describes the nature and order of the elementary transformations included in the matrix. Always think of how you will deal with changes in the future. In ten years time, you should be able to glance over the script to remind yourself how it works, then make any required change without having to re-work the math.
Chapter 22: Quality Control - Scripting Style 397
22.1.2
Structure your Programs
Structure your applications to provide clearly defined parts and functions. Changes made to one part of a script should have as little impact as possible on other structures in the program. Structured scripting is discussed in chapter 8 Scripting Style and chapter 9 Macros and Sub-Routines.
22.1.3
Use Robust Resources
The single easiest way to make an application that is buggy and unreliable, is to use buggy, unreliable resources. Conversely, to make a robust application, you must use the best resources at your disposal. You should develop and maintain a pool of high quality re-usable resources that can be included in any of your scripts. Use these resources wherever possible. With every macro or re-usable sub-routine you develop, you should also write documentation that clearly explains what the resource does and how to use it. This documentation will be invaluable as new programmers are introduced to your expanding business. It will also be useful to refresh your memory if you haven’t used a particular resource for a few months.
22.1.4
Build Error Handling into your Scripts
It’s the easiest thing in the world to accidentally enter invalid data into a parameter field. As programmers, we must allow users to make this type of mistake without it causing unpleasant results. Example: Choosing a Pen Every line, circle or polygon in plan view, and every outline of the 3D model, uses a pen from the current palette. To set a pen for a particular element, use the GDL command pen pen_index, where the pen index is an integer between 1 and 255. To see how this works, create a new object with a pen parameter penIndex and set its default value to 3. Copy the following GDL script into the object’s 2D Script editing window. !Set a pen index, and draw a line pen penIndex line2 0, 0, a, 0
Click the 2D Full View button to run the script. The color and thickness of the line matches the penIndex parameter.
Figure 353 – When selecting a pen, the end user is free to select one of 256 pens plus the transparent pen (index = 0) or the ‘background colour’ pen (index = -1).
When the user selects a pen index, a palette is presented (figure 353). The user must click on one of the colored squares in the palette to select the desired pen.
398 Chapter 22: Quality Control - Scripting Style The problem occurs when the user chooses a transparent pen or the background pen from the pens palette. These extra pens are provided for fill backgrounds. For fill backgrounds, pen index 0 indicates a transparent background, while the pen index -1 matches the window background. In the example object, choose pen index 0 for the value of the penIndex parameter. Click on the 2D Full View button to run the script. An error message will pop up (figure 354). The same error message occurs if you select the pen index -1. To avoid this error message, we need to provide some error handling.
Figure 354 – If either the transparent or
One approach might be to restrict the pen values using the values command in background pen is used directly in a script, a warning message will come up when the the Parameter Script. To see the result of this approach, copy the following GDL script into the Parameter Script window of the object.
script runs.
!Restrict pen index to a value between 1 and 255 for i = 1 to 255 put i next i values "penIndex" get(nsp)
Now try selecting a pen value. You will see that the selection palette has changed shape (figure 355). This is not a very satisfactory solution, as it results in a rather confusing change in the way the pens are presented to the user. It’s better to allow the user to select any pen, and then provide error handling in the 2D Script to deal with zero and negative values. This opens up the option of allowing the user to select a zero pen to omit parts of the 2D symbol. Change the 2D Script of the example object to the following: ! Set a pen index, and draw a line if penIndex = -1 then penIndex = glob_drawing_bgd_pen if penIndex then pen penIndex line2 0, 0, a, 0 endif
Figure 355 – If you use a values list to restrict the pen selection, the palette changes shape.
Chapter 22: Quality Control - Scripting Style 399 Now if you select pen -1, the line will be drawn in the pen that most closely matches the color of the screen background. If you select pen 0, the line will not be drawn at all. With this error-handling in place, you can select any pen index you like. It is easy to implement, and I would recommend that it is used wherever a pen is set in a GDL script. Try it Yourself: Section Fills When you define a section fill in the 3D Script, you must specify the pens for the cut contour, fill and background. Errorhandling for pens is discussed above. The final attribute required by the sect_fill command is the name or index of the fill pattern. It’s not quite so easy to ensure the existence of a fill. There may be any number of fills, each with a somewhat arbitrary index. If you were to import the same fill pattern into two projects via the Attributes Manager, it’s index may (and probably will) have a different index in each project. Worse yet, if you were to specify a missing fill index within the 3D Script, an error message would come up each time that 3D Script were to run. If 100 instances of the object were placed in a project, 100 error messages “Fill not found at line nnn of file NNN.” would come up each time you generated a 3D, section or elevation view. So it’s important to avoid ‘missing fill’ errors, but practically quite difficult. One way to avoid such errors is to refer to fills by name, and to re-create them if they are missing. This can be done in a Master_GDL script, as illustrated in the example below: !Create a Solid Fill and an Empty Fill if not(ind(“Fill”, “Solid Fill”)) then define solid_fill “Solid Fill” endif if not(ind(“Fill”, “Empty Fill”)) then define empty_fill “Empty Fill” endif
However, it turns out that ArchiCAD users don’t like GDL programmers to create fills for them. So we must search for an alternate approach to safeguarding from the ‘missing fill’ error message. I tend to use the following approach: 1. Check for the existence of the fill. 2. If the fill does not exist, do not include the sect_fill command. Existence of a fill can be checked using the “Name_of_Fill” request as in the following example: !Check whether a Fill Exists fillName = “”
400 Chapter 22: Quality Control - Testing rrr = request(“Name_of_Fill”, frameFill, fillName) if rrr = 1 then sect_fill frameFill, frameBkgdPen, frameFillPen, frameSectPen endif
In the above script, the existence of the fill is tested by checking that the request returns a value for the fill name. If no value is returned, the fill must be missing, so the sect_fill command is skipped. Note that the variable fillName could be initialized in a header.
22.1.5
Acceptable and Unacceptable Errors
Some errors are more acceptable than others, depending on how they affect the user. For example, if ArchiCAD freezes up every time a user enters a particular value, that is totally unacceptable as it can result in significant loss and frustration to the user. If an error message pops up when an unusual value is entered, that is far less critical. You could spend a lifetime providing error handling for obscure and trivial cases, but there are many more interesting and important things to do. I would suggest that you identify the critical error handling, and the error handling for which you have standard solutions. Ensure that all your scripts include error handling for these cases, and perhaps also for some of the more common foreseen user inputs. Anything else is somewhat optional. I may be shot for saying this, but to be quite blunt, there’s no point trying to make your software fool proof – you may achieve perfection but at what cost?
22.2 Testing Typically, development and testing are carried out by different people. The mysteries of testing are beyond my ken, although I have do carry out informal testing as required. This section then should be taken for what it is – my personal thoughts on some aspects of testing, based on observation and limited experience. Ideally, there should be as much resource put into product testing as there is into development. The important thing about testing, however, is how to squeeze the maximum benefit out of the time allocated. In the following paragraphs I will outline some approaches to testing that I have found most useful.
22.2.1
Test as you Work
As you (the programmer) add functionality to an application or resource, test it then and there. You will usually find issues that can be quickly isolated and fixed. If you leave it till later, finding the source of the bugs will be more difficult.
22.2.2
Exploratory Testing
When testing a change to a particular function of a product, first test the change directly. Often the change will have been made to provide a specific improvement or to fix a specific bug. This should be tested first, to see if the specific goal has
Chapter 22: Quality Control - Testing 401 been achieved. But testing should not stop there. Any change will likely affect a range of related functionality. With knowledge of how the product works, you should explore all possible related errors that you can think of. Also explore ArchiCAD contexts such as storeys, layers, listing and scheduling, sections and elevations, 3D, photorendering and floor plan display modes. Test the complete user interface if appropriate, including the Settings dialog, dynamic hotspots (in 2D, 3D, section and elevation) and selection & referencing hotlines and hotarcs. As a tester your attitude should be one of wanting to find issues. Assume that the bugs are there, and make it your mission to find them by hook or by crook.
22.2.3
Test by Presentation
As a new software product (or a bunch of improvements to an existing product) reaches completion, one of the most effective testing techniques I know of, is to present the product to a panel. This is sure to expose issues, as you explain how the software works (or is intended to work at least), and as the panel members ask questions about the product. I’m not sure what it is about such a forum, but it never disappoints. You will find that a surprisingly large number of issues will be exposed in a relatively short time frame. Having experienced this phenomena once or twice in a non-testing environment, I eventually recognized its potential as a testing mechanism. At Cadimage, we now use this technique deliberately and frequently.
22.2.4
Automated Testing
Automated testing can be achieved via macros that replicate user inputs. After looking into this approach, I don’t think it is that practical for GDL objects. There are significant difficulties with setting up the tests in such a way that they can be replicated consistently.
22.2.5
Test on Different Platforms
Check that there are no platform-dependent issues. For GDL objects, the most common difference between Mac and Windows operating systems is the Settings dialog. Interface elements that work fine on a Windows platform may not fit the allocated space when run on a Mac platform. Check this in particular, and if necessary increase the size of the fields, and the space around fields. Most of these issues will be avoided if you follow the guidelines for designing a Settings dialog as discussed in the earlier chapters.
22.2.6
Documentation
As you create the illustrations for the software reference manual, many issues will surface. To create each illustration, you
402 Chapter 22: Quality Control - Fixing Issues need to run the scripts with different settings. This forces you to use the software in different ways, and can yield some useful test results.
22.3 Fixing Issues The process of fixing issues is fairly basic. It breaks down to two parts. First, identify where in the script the issue occurs, and then edit the script so that it works correctly. How to achieve the second part of this process will depend on the exact nature of the issue. Sometimes it will be a matter of correcting some syntax. At other times it may be necessay to allow for a special case, or in more extreme cases re-design a complete algorithm. In the worst case scenario, the product’s structure may not permit the issue to be resolved efficiently, and you may have to re-design the product from the ground up. Good design should mean that most of the bug-fixing you carry out will be of the simplest kind. In this section I want to focus on the first part of the process, namely techniques for finding the location in a GDL script where a given issue occurs.
22.3.1
Have a Guess
My favourite technique for finding a bug in a GDL script is to take a guess. I suppose it’s a bit like a doctor examining a patient. By careful consideration of the symptoms, a diagnosis may be arrived at and a cure prescribed. In GDL scripting, the symptoms will often suggest a cause. By carefully examining the script at the suspected point, a syntax error or a logical error will usually be spotted.
22.3.2
Get Output from the Script as it Runs
It is often helpful to examine the value of a variable at a given point in the script. For most of the scripts, using the print statement will report the variable value as the script runs. By comparing the reported value with the value you expect, you can often test which of many variables may have been incorrectly set by the script. The variable can then be tracked back through the preceding lines of script to find the source of the error. The Parameter Script is notoriously difficult to debug. The problem with this script is that you cannot use the print command to examine variable values. To get around this problem, output data to a text file using the following script fragment. testChannel = open(“text”, “test.txt”, “mode = WA”) output testChannel, 1, 0, comment, variable_name close testChannel
Admittedly this is a bit long-winded, but it’s the most direct way I know of examining the variables. Be aware that the
Chapter 22: Quality Control - Optimising for Speed 403 Parameter Script runs multiple times for every time a parameter value changes, and also at other times. This means that the data output to the text file will be repeated, so can be a little hard to understand. To avoid unnecessary processing when the Parameter Script runs, set the glob_modpar_name variable to the empty string at the end of the script as described in the previous chapter. One other way to avoid issues in the Parameter Script is to avoid setting parameter values in macros. When you call a macro, it should return the adjusted values to the main object, and let the main object update its own parameters. This is discussed in chapter 9 Macros and Sub-Routines.
22.3.3
Simplify
Occasionally an issue will emerge that defies any attempt to pinpoint its location. At this point you could consider simplifying the script by selecting the entire script and changing the whole thing into a giant comment. By slowly removing the comment command from ever increasing portions of the script, eventually you will find the point where it fails.
22.3.4
Infinite Loops
Infinite loops are quite a problem. When you start a project, chances are the script will run (if it is the 2D Script, master Script or Parameter Script), which will freeze ArchiCAD. In situations like this, create a new project that does not load the buggy object, then open the object from its library and fix the bug.
22.3.5
Borrow an Extra Brain
Two heads are better than one. From time to time you will encounter bugs that seem to defy all efforts to locate. I suppose that every programmer has a level of expertise that he or she no longer bothers to check. Over the years, we get to know our personal weaknesses, and subconsciously run through a checklist of possible sources of error. For myself, spelling is never a problem, so as a result I rarely think of checking this as a potential source of error in my scripts. This means that when I do make that rare spelling error, it can sometimes take a while to find it. It turns out that the simple process of explaining your script to another human can be enough to highlight the offending script. I don’t believe it even has to be a programmer. All you need is someone who is prepared to ask questions. As you explain what your script is supposed to be doing, the bug will jump out at you. It’s a great option to keep up your sleeve as a last resort.
22.4 Optimising for Speed Sometimes the complexity of an object’s 3D model, or the amount of processing required to perform a set of calculations,
404 Chapter 22: Quality Control - Optimising for Speed may cause a perceptive slow-down. This can be tiresome for a user, especially if many instances of the object have been placed into the project. The problem can be amplified during graphic editing. A slow object will respond even more slowly, as it is continuously rebuilt during the edit session. Generally, anything that causes an unnecessary loss of performance should be avoided. For simple objects that are used infrequently, you may get away with redundant processing and inefficient algorithms. If your objects become sluggish and frustrating to use, you should seriously look at your scripts and try to find ways to improve their performance. Even if individual objects are relatively agile, gains in performance across multiple objects can make a big difference. Our focus here goes beyond providing bug-free code. Whenever possible, you should factor in some time for optimization. There are a number of strategies you can use to optimize your script. In this section we will touch on some of these techniques. Generally you will use a combination of techniques to optimize your objects.
22.4.1
Testing for Efficiency
Before we start, we should define what we are trying to achieve, and how we can gauge success. Very simply, we want our 3D Scripts to run faster. Generally speaking, the 3D model tends to be the most important script to optimize, as this script generally requires the most processing power. A 3D model can contain thousands of polygons. Whenever a user creates a 3D view, all of the objects and building elements are converted to 3D, often resulting in hundreds of thousands of polygons for the 3D engine to process. The test iteslf is quite simple. Essentially, we place multiple instances of our object into a project, and generate a 3D view. We time how long the 3D view takes to generate. When we apply an optimization strategy, we re-run the test. If the view takes less time to generate we judge our efforts to have been successful. It’s a good idea to establish how many instances of an object you might expect to be used in a project. If the object is a bath tub, we might expect two or three to be placed in a residential project, or perhaps a hundred or so for an apartment block. If the object is a small flowering plant, we can expect that a few hundred might be placed into a regular sized project. It’s important to establish what would be considered a reasonable generation time. If a designer has to wait 2 seconds for each of 500 small flowering plants, that will be an extra quarter of an hour of his time gone each time he generates a 3D view of the site. Evidently this is too slow. A more reasonable time frame might be 1 or 2 minutes, so we should aim to generate at least 5 plants per second. On the other hand, a designer might be prepared to wait 1 or 2 seconds for a large tree to generate. Once these two questions have been answered, place the required number of objects into a plan view, and generate a 3D view, noting the time required for the view to generate. This will give you an indication as to whether improvement is required. It will also the base time measurement against which to measure that improvement. Usually we will want to experiment with a number of optimization techniques. As each technique is applied, we should
Chapter 22: Quality Control - Optimising for Speed 405 run the exact same test. If the technique was successful, the improvement in generation speed may be massive. It is not uncommon to see improvements of 10 times the original speed. It’s easy to be fooled into thinking you have achieved a massive improvement, only to find that the 3D window did not completely re-build. Some of the 3D model gets cached or pre-processed by ArchiCAD so that less processing will be required the next time a 3D model is generated. For a true speed test, you should first select all the objects and change a parameter value (this can be a dummy parameter that does not affect the 3D model). This will effectively force a rebuild of all the placed instances of that object. To time the process, use a stopwatch - don’t rely on ArchiCAD’s report. The report only lists some of the processing time. Other time is ignored. One glaring example is when you use a picture polygon. Try it, and compare the time on your stopwatch to that recorded in the session report window. Note that the smarter ArchiCAD gets (doing pre-processing during down time) and the smarter computers get (multicore processing) the more difficult it is to run a true test of efficiency.
22.4.2
Efficient Algorithms
One way to make a script run more efficiently is to look more closely at the blocks of code it contains. Each block of code is designed to perform a particular task. By performing each of the various tasks more efficiently, the program as a whole will run more quickly. Sometimes there are obvious improvements that can be made. For example, when sorting large quantities of data, you can use an efficient method or algorithm such as Heap Sort rather than simple sort. When searching within sorted data, use a bisection method rather than checking each record in turn. In other cases, it can be more difficult to find ways to improve an algorithm. But isn’t all of this just a classic case of re-inventing the wheel? If an algorithm works, why mess with it? A good point, perhaps, but imagine if someone really did re-invent a wheel that just worked better! Chances are, the first wheel ever invented lacked sophisticated bearings, pneumatic tires and disc brakes. Usually an efficient algorithm is less obvious than an inefficient one. As a result, many programmers script inefficiently. They are under pressure to complete a project, and simply don’t have the time to make their scripts hum. While this is the harsh reality of a programmer’s work experience, there are some cases that demand efficiency. In such cases, it’s good to have a few strategies in place to help identify significant inefficiency and improve it. What makes an Algorithm Efficient? An algorithm is efficient if it uses a small number of operations. Whenever you script an algorithm, you make design decisions. How do I calculate the intersection of two lines? How should I measure the length of a curve? How can I orient the local axes to point in the desired directions? Depending on your approach to solving such problems, you may use more or fewer calculations.
406 Chapter 22: Quality Control - Optimising for Speed If you take a look at the 27 Split Edges sub-routine presented in chapter 21 Polygon Operations, you’ll see that each edge of polygon A is compared with each edge of polygon B. If polygon A has 4 edges, and polygon B has 3 edges, the script block that compares the edges will run just 12 times. However, imagine if polygon A has 100 edges, and polygon B has 129 edges. Now the block of script will run 12,900 times. Each time the comparison script runs, it performs a constant number of operations. For two polygons with m and n edges, then, the number of operations required to run the 27 Split Edges sub-routine is k(m*n). Use Smart Algorithms Smart algorithms already exist for numerous common tasks. Research these algorithms, and use them wherever they can make your scripts more efficient. In other cases, a bit of thought can be applied to come up with your own smart algorithms. Think about how you could approach the same problem in a different way. Calculation vs. Iteration Yes, your computer can perform 17 gazillion calculations per second, but that’s not a good reason to make it do unnecessary work. If you’re searching within an ordered set made up of thousands of data, use a bisection search to find the data in 10 steps rather than taking 2000 steps to check each record in turn. Don’t rely on an iterative approximation method when a little more brainwork will yield an analytic solution. For example, wherever possible calculate a sum or an integral directly instead of adding small sums to find an approximate answer. Calculating the sum or integral is like calculating the number of bricks you need for a house, and having them all delivered at once. Using an iterative approach is like ordering each brick and having it delivered to site separately. Avoid Unnecessary Calculations This seems kind of obvious, but it’s not always that easy in practice. In the 27 Split Edges sub-routine (chaprer 21: Polygon Operations), there are quite a few redundant calculations. Whenever the sub-routine finds an edge that splits, it immediately adds it to the polygon. This means moving all the edge data to make a space for the new edge each time an edge is split. It also means that the number of edges in the polygons increases while the algorithm is running. A more efficient approach could be developed. First, a list of split points would be found for each edge. The list would be ordered by the split point’s distance from the edge start point. Finally, all the points would be inserted into the polygon at once. The result would be a bit more complicated, but it could save valuable processing time. It’s probably worth going the extra mile in this case, as the sub-routine is used by nearly all the polygon operations. Which brings me to my next point.
Chapter 22: Quality Control - Optimising for Speed 407 Spend Time on Important Algorithms When improving algorithm efficiency, you want to make the best use of your time. You probably don’t want to spend much time on algorithms that are used only once in a script, and involve only a handful of calculations. There are two types of algorithm that you should concentrate on when optimizing your GDL scripts. Complex algorithms that contain multiple lines of code, especially if they include loops, are usually a goldmine of inefficiency. Search for complex-looking blocks of code. If it looks complex, it probably is. If it is complex, the original programmer almost certainly has not had much time to make it efficient. Algorithms that are used frequently throughout your GDL scripts should also be scrutinized. While they may be relatively small and insignificant in themselves, such algorithms accumulate importance through repetition. Use Appropriate Loops Use while-endwhile or repeat-until loops rather than for-next loops if this will reduce the number of iterations. Only use for-next loops if you know how many iterations you will require. A typical scenario might be if you were searching through a list of unsorted data. As soon as you find what you are searching for, you could exit the loop. A ‘for – next’ loop would continue searching to the end of the data, even after a match was found. Use Arrays Thoughtfully Each time you create a new variable in a GDL script, a tiny but finite amount of processing time is used. Usually this is insignificant compared to other time-wasters, but you should be aware of the potential slow-down. The slow-down is particularly true of array variables. Each time you dimension an array variable, the GDL interpreter slows down by a surprising amount. This is especially true if it is compounded by a loop. Unless there is a particular need to minimize the size of the array at all times, it should be declared once then re-used multiple times, rather than being declared at each iteration of the loop.
22.4.3
Calculate Only when Required
When you create a 3D object, there is almost always a certain amount of calculation. Unless a parameter value is changed, the results of every calculation will be identical each time the script is run. It seems rather pointless to re-calculate these values. Instead, the calculations could be performed (whenever the value of a parameter is edited), and the results stored in the parameters of the object. This can make a massive improvement to the over-all speed of the scripts. A while back I designed a range of 3D tree objects in which the calculations alone took between 1 and 2 seconds to perform. Initially these calculations were being performed each time the 3D model was generated. This had a profound
408 Chapter 22: Quality Control - Reduce the Polygon Count impact in generation time. Storing the results of calculations poses its own set of problems however. If 2 or more objects are selected while editing parameter values, the selected objects will all end up the same irrespective of their individual parameter values. Cadimage has had to create an add-on to combat this.
22.5 Reduce the Polygon Count When a 3D model contains a high number of polygons, you should consider ways in which the polygon count can be reduced. A low polygon count does not directly correlate to an efficient object. The shape of the polygons must also be considered, especially if the polygons contain concavities. For example, when creating a cardboard cutout type object, say a tree, the polygon should not contain more than a few hundred edges, or it will be inefficient. On the other hand, bodies composed entirely of triangular polygons are far more efficient and you can safely include a few thousand polygons. However, given two polygons comprising exclusively triangular polygons, the one having the least number of polygons will always generate more quickly.
22.5.1
Use Primitive Elements
If you want total control over the number of polygons in the model, you may consider using primitive elements to build the model. Store the vertex, edge and polygon lists in array parameters. If you do so, the calculations used in defining the polygons are removed from the 3D script and will run only when required. Note that if you store triangular polygons, you can always be certain that they will be planar. Storing the primitive elements in array parameters also allows you to easily link the edges to the polygons. As you create each polygon, update the edge list to identify the polygons associated with each of the edges. For some reason, in ArchiCAD 14, it is impossible to assign the very last polygon to an edge. Leave this one as -1 in the edge list.
22.5.2
Avoid Unnecessary Detail
Some models can be simplified in some situations by removing hidden elements. For example, in 3D a glass-washer with its door open can be made to look more realistic if it includes a dish tray. However, if the door is closed, there is no benefit to be gained by including the dish tray. This simply adds an unnecessary toll on the 3D processor. The simple solution is to remove the tray if the door is shut.
22.5.3
Limit Automatic Curve Resolution
Any 3D curve is represented as a set of small planes or facets. The number of facets used to represent circular arcs (defined using status codes), can be controlled using the resol and toler commands. This also affects standard curved
Chapter 22: Quality Control - Reduce the Polygon Count 409 elements such as cylind, ellips and sphere. In an earlier chapter, we saw how to approximate mathematical arcs with a series of short line segments. Whether or not this approach is followed, you will need a relatively large number of flat polygons to plausibly represent a curved surface. Whenever you view an object that contains curved surfaces in 3D, the imaging will be relatively slow. When you use such an object as part of a solid element operation, it will take extra time. Use resol or toler to limit the number of polygons. You may have to use several different resolutions (resol or toler) throughout your script. For example, a cylinder is by default made up of 38 planar surfaces (36 polygons to represent the curved surface and two end caps). Imagine you are creating a clothesline that uses cylinders to represent each of 20 wires. That’s a total of 760 polygons for the wires alone. This is an extravagant number of polygons to use just to draw some wires that will hardly be visible except as a thin line on a photo rendering. I would suggest cutting the polygon number down to 7 polygons per wire, or 140 for the whole set. The amount of processing required to produce the 3D view will be dramatically reduced, and there will be no reduction in image quality. Using Resol
Figure 356 – Using a resolution of 36 results
The resol command allows you to set the number of planes used to model in regular curved surfaces . the curved surface a complete cylinder. The syntax for this command is: resol n_planes_in_a_complete_circle
Try it Yourself: Using Resol To see how resol works, create a new object, and type the following GDL script into the 3D Script editing window. resol 36 cylind 1.000, 0.300 addx 0.700 sphere 0.300 del 1
Click on the 3D View button to view the 3D model you have created. You will see a perfectly ordinary looking sphere and cylinder (figure 356). Right-click in the 3D View window, and choose the 3D Window Settings to Figure 357 – Reducing the resolution to 6 use the Internal Engine, and Shading Mode. You should be able to see how the results in somewhat chunky curves.
410 Chapter 22: Quality Control - Reduce the Polygon Count two elements are built up of a net of small planes or facets. If you were to count the planes used to define the curved surface of the cylinder, you would find that there are 36. If you were to count the planes used to represent the curved surface of the sphere, you would find that there are 36 x 18 = 648 planes. Now change the first line of your 3D Script to read as follows. resol 6
Click on the 3D View button once more to see the result of the change you have made. The 3D elements now use fewer polygons. The cylinder has a hexagonal cross-section, and the sphere looks very prismatic (figure 357). Curved 3D surfaces are only ever approximations – in this example, very rough approximations. For very thin cylinders or tiny spheres, this level of approximation may be sufficient. For larger elements, it probably is not. Using resol, you can choose a resolution suitable for each element you use. Using Toler Toler is perhaps an even more useful tool, as it specifies the maximum distance that the approximation may deviate from the true curve. The syntax for this command is as follows: toler max_deviation_from_the_curve
For instance, the following script states that any curved surface must be represented by planar facets that must not deviate more than 1mm away from the true curve. toler 0.001
Try it Yourself: Using Toler To see how toler works, create a new object, and type the following GDL script into the 3D Script editing window. resol 36 gosub "Shape" addz 0.500 toler 0.0005 gosub "Shape" end "Shape": R1 = 0.020 R2 = 0.300
Figure 358 –The toler command (upper) uses fewer edges for smaller radius curves. The resol command (lower) ignores the radius.
Chapter 22: Quality Control - Reduce the Polygon Count 411 qi = 5 prism_ 6, 0.100, 0, 0, 900, -R1*cos(qi), R1*sin(qi), 79, 0, 180, 4079, R1*cos(qi) + 2*R2*cos(qi), -R1*sin(qi), 1079, 3*R1*cos(qi) + 2*R2*cos(qi), R1*sin(qi), 1079, -R1*cos(qi), R1*sin(qi), 1079 return
Click on the 3D View button to view the 3D model you have created (figure 358). If you examine the two prisms carefully, you will see that the lower one (created using resol) has a large number of polygons around the tight curves at the ends, and about the same number to define the large radius curve. The tight curves appear relatively smooth, while the large radius curves appear more fragmented. The curve at the top, created using toler, is more uniform. Both the large and small radius curves are defined at a similar level of detail. To see the difference more clearly, edit the script so that the value of resol is 12, and the value of toler is 0.004. Run the 3D Script once more to view the result (figure 356). The imbalance between the resolution of the small-radius and large-radius curves on the lower prism, is now more evident. The tighter curves still look Figure 359 – The difference between toler (upper) and resol (lower) is even more clear at quite smooth, while the large radius curves appear jagged and ungainly. In contrast, the upper prism uses the same number of polygons to define its curved surface, but the effect is much more uniform.
low resolution.
A Rule of Thumb Especially in cases where a single element includes a mixture of tight and wide curves, toler is preferable. This is also true when the exact dimensions of a curved element are parametrically defined. A cylinder should automatically gain resolution (i.e. use more polygons to define its curved surface) as its radius increases, and lose resolution as the radius decreases. This situation is very easily achieved using toler. As ArchiCAD is dedicated to producing architectural objects, a tolerance of 1mm is usually sufficient for every object. In some cases, you can increase the tolerance to 2 or 3mm.
412 Chapter 22: Quality Control - Reduce the Polygon Count Try it Yourself: Choosing a Sensible Value for Toler To see how this works, create a new object, and type the following GDL script into its 3D editing window. toler 0.001 R = 0.001 repeat addy R cylind 2, R R = 2*R addy R until R > 1
Click on the 3D View button to view the 3D model you have scripted (figure Figure 360 – When toler is in effect, the 360). smaller the radius the fewer polygons are used If you fit the whole 3D model into the view, you will get a feel for a typical to define the curved. As you can see there is no close-up Architectural view. This is the scale at which photorenderings, fly- appreciable reduction in visual quality. throughs and VR sequences will be shown. As you can see, by using a tolerance of 1mm, the resolution of all the cylinders looks good (it would look better had we used the OpenGL engine). We could have used resol to achieve a similar result, but we would have had to set the resolution of each cylinder independently. Had we used the default 36 planes, the large column would have looked chunky, and the thin wire would have been unnecessarily detailed (figure 361). Define the Number of Polygons Explicitly We saw in chapter 16 Approximating Curves, how you can control the exact points to include on a curved surface. This approach can be used for curved surfaces defined using GDL primitives, or by points on a body defined by a polyline (provided you don’t use Figure 361 – When resol is in effect, all similar automatic arc status values). curved surfaces are constructed using the same This provides you with an even greater level of control. You can choose a number of polygons. sensible number of polygons for any given curved surface.
Chapter 1: - Provide Optional Data 413
22.6 Provide Optional Data When modeling using 3D primitives, there are a number of situations that you can choose either to provide data, or to let the GDL interpreter calculate the data for you. If you choose the second option, you may find that the GDL interpreter has to do a significant amount of extra processing, effectively slowing the 3D model generation time. To avoid this slowdown, it can be a good idea to take the extra time and effort necessary to provide all the optional data. This is especially important when creating surfaces that contain a large number of polygons. If you inform the GDL interpreter as to which polygons belong to each edge, you will notice a marked improvement in performance.
22.7 Use Group Operations Sparingly Group operations are cool, but they are not the answer to everything. The reason for this is that the processing behind them is quite high-powered. Over-use of groups can quickly lead to an unacceptable slow-down. Only use group operations if it makes for better objects (or significantly more readable scripts) that retain good 3D generation speed. If there is an appreciable drop-off in performance, you should consider other ways to define the model.
23 Development Strategies As a programmer, it can be easy to become so involved in the daily challenges of creating applications, as to lose sight of where one is headed and how one intends to get there. Situations arise from time to time that remind us of the bigger picture, and require us to make decisions that will affect future software projects. An opportunity may emerge that we could grasp if only we had the expertise, the time, or the resources. A problem may come up that threatens the very foundation on which we have designed our software. It is easy to respond to such situations with a sort of damage control response. We may be tempted to cut corners to get a job out of the door. We may find ourselves fixing issues that could have been avoided by better planning. It is true that there will always be an element of surprise in our day-to-day work. This is one of the attractions of programming. Just when you think you’ve arrived, new possibilities emerge. However, we’ll see that there are ways to manage development that can help to reduce uncertainty and risk, and that will result in better products being developed on schedule. It seems to me that a successful GDL developer is like a good farmer. The farmer regularly takes quality produce to the market. This will pay for the daily running of the farm. To achieve this, she ploughs the fields, plants the seed, irrigates, and finally harvests the crop. But, as a successful farmer, she is also constantly making improvements to the farm. She keeps abreast of advancing technologies. Improved fertilizers might promote better growth; a new harvester might take a greater percentage of the crop. She minimizes risks, building better fences to keep the cows out of the corn. She might hire more farm hands, try different crops, or even expand her farm by buying more land. A GDL programmer should have the same attitude to his work. He wants to not only take good product to the market, but also to improve his ability to develop more efficiently and in different areas. In this chapter, we’ll look at ways to manage development so that we can deliver product in a timely fashion, while ensuring continued and increased success in the future. I’ll try to develop a general model for development that can be applied to all aspects of the process. 22
22
Sustainable GDL development can occur only where there is a demand for the product. This demand may be driven by in-house development for a large architectural office where internal systems are being improved. It could stem from the wider ArchiCAD community, where generic and niche third-party products may be marketed to a wider clientele. The demand might be simply a personal interest in creating cool stuff. In this book, I have not touched on the challenges of establishing a market for the product. Sales and marketing do not form a part of my skill set.
Chapter 23: Development Strategies - A Model for Development 415
23.1 A Model for Development To my mind, there are two parts to GDL development: creating product, and improving the resource pool.
23.1.1
Goal
The first thing that any developer needs is a well-defined goal. This is crucial, as it provides direction and also a yardstick to measure success. In practice, you really need two goals. One of these is a general goal that you can use to develop your effectiveness as a programmer, and to measure your level of personal satisfaction. It is very rewarding to come to the end of a project, and to feel a sense of pride in the work you have done. The other goal you will need is project specific. Each project places certain practical demands and constraints on time and resource that will effect how you apply your personal goal. You need to be aware of these modifiers, while maintaining your integrity. Thus, a clearly defined goal will also motivate the development of strategies to achieve that goal.
23.1.2
General Goal
The goal of each GDL programmer differs less in content than in focus. The basis for the general goal is the production of libraries of GDL objects. When you develop your general goal, consider aspects of quality and practicality. If you can afford to spend a lifetime producing a perfect library, that is all well and good, but its relevance may well be outdated by the time you are done. There is always a tension between quality and quantity that comes from the reality that we must eat to live. The software must be developed within budget, and yet be of sufficient merit that it is worth buying at the ask price. Common elements of a typical general goal would include the production of objects that are: •
Robust – they always work and have no bugs.
•
Well-scoped – they work as they are intended.
•
Flexible – they provide design options.
•
Intuitive – the end user finds the interface easy to understand.
•
Productive – the end user can work more efficiently when using the objects.
•
Aesthetic – they look good when used in an ArchiCAD project.
•
Developed efficiently – each object and library is developed within budget.
•
Protected – users must purchase the right to use the objects.
It is essential to develop an over-all or general goal that spans your whole development. It will probably contain all of the above elements. The priority you assign to each, however, is very personal, and extremely important.
416 Chapter 23: Development Strategies - A Model for Development If your goal is to provide tools that increase the efficiency of the architectural design office at which you work, you may decide that your primary, driving goal is productivity. You may be prepared to sacrifice flexibility, and impose certain methods of working, in order to achieve this. Similarly, you may not be overly concerned with producing software that is intuitive or even particularly robust, since you will be on hand to provide support when it is needed. On the other hand, if you are producing software for the general ArchiCAD community, your priorities will change significantly. In this case, it is paramount that your software is robust, as it will be in constant use around the clock by hundreds or even thousands of people. Similarly, an intuitive user interface will be high on your list of priorities, as each end user will have their own way of working. Having prioritized these items high in the list, you also need a point of difference for your product. Are you aiming to sell to designers or draftsmen? If your target market is designers, then flexibility or aesthetics may rank higher than productivity. If you produce libraries for individual clients, you will need to prioritize development efficiency more highly than either an in-house or a speculative developer. The client will always want as much value for as low a cost as possible. This goal is ‘general’ in the sense that it will apply generally to all your development projects, not in the sense of being imprecise. It is in fact very precise – this is essential, as it is the one thing that provides a steady direction for your product and personal development. Re-assess and refine your general goal regularly. It’s unlikely that this will result in a complete U-turn – that would be fairly disastrous, although if it needs to be done it should be done promptly. More likely, you will fine-tune your goal, or even decide to leave it un-touched. A good time to re-assess your goal is a few weeks after the completion of a project, when you have obtained feedback from your clients.
23.1.3
Project Goal
Within the framework of the general goal, each project will have its own specific goal. This goal will be directly related to budget, but also to the requirements of the project. Evidently the perfect tool would fulfill all of these objectives. In practice however, compromises must be made. Total design flexibility begins to impact on productivity and certainly on development speed. In some cases it is possible to produce tools that are both flexible and intuitive. A goal for a specific project might, for instance, prioritize productivity, design flexibility, and software that works well within a clearly defined scope. You will need to make decisions on the scope of the project, including what features could be included later. The critical requirements must be met, and these will be defined by the project goal. Non-critical features may have to be left for a later upgrade, depending on the budget.
Chapter 23: Development Strategies - A Model for Development 417
23.1.4
Progress
The most tangible result of development is project completion. This is the most obvious way to measure progress, as it is the end goal of all development. Without it, development could not be sustained, as only the promise of a completed project will convince a potential client to commit. Development progress cannot be measured merely in these terms, however. While a set of similar projects or tasks can often be completed at a constant, linear rate, this view of progress would have progress stop as soon as a new problem is encountered. To solve a problem takes time, and hence delays project completion. And yet the problem must be solved for progress to continue. Again, there is frequently significant overlap between projects. A problem solved during the course of one project can often result in a resource that can be used in other projects. The solution effectively initiates a new burst of progress. New resources are required in order to develop each new application. Many of these resources can be re-used in future projects, and can often be plowed back into existing applications to produce upgrades. Frequently, unexpected or planned combinations of existing and new resources can lead to new fields of opportunity. It’s difficult at times to identify how best to use your non-renewable resources (time, personnel and money). A good model for development is an expanding cycle. As new resources become available, new opportunities arise, and new applications are developed. Development leads to new resource. Resource leads to new opportunity. Opportunity and resource combine to initiate, direct and facilitate development.
23.1.5
Development Strategies
Development strategies can be simply derived from a model of the development cycle, and an understanding of the project lifecycle. The Development Cycle I like to think of the development cycle as a massive flywheel. Each new project uses existing resources. In addition to these resources, some new resources will usually be required. Having created the new resources, the pool of resource is now greater. Opportunity arises from the increased resource pool, leading to further projects, and so the cycle repeats. With every turn of the wheel, a new application is completed. By-products of each cycle are new resources and opportunities. You might expect that the wheel would get more difficult to move with every turn. The weight of the new resources should surely add to its inertia. On the other hand the new resources might be expected to make development easier. As it turns out there is generally a balance between these effects. The wheel continues to grind on at a steady rate. The Project Lifecycle The lifecycle of a specific project proceeds in a well-defined way. First, an opportunity is identified. This might arise from
418 Chapter 23: Development Strategies - Opportunity a request, or from an insight on the part of the developer. At this stage, any new resources that might be required are identified. The project is then planned. This will include a detailed assignment of resource, budget considerations and a time-line. The required resources are gathered. New resources are obtained or created as required. There is then a period of implementation in which the software is programmed. This is followed by a period of testing and bug fixing. Documentation and packaging are produced. Finally the product is delivered.
23.2 Opportunity Ideas for new developments can be surprisingly easy to come by. They come from a variety of sources. Some will be quite original, others a result of requests from users. Again, new or improved resources can spawn a host of new possibilities for new and improved libraries. I have never experienced a time when ideas for development were scarce. In fact, there are usually so many great ideas bouncing around that the difficulty is maintaining focus, and not getting lured into the trap of taking on multiple projects at once. Prior to planning a development, it’s a good idea to analyze the expected outcome. How long will it take? How many units need be sold before it is profitable? Does it require major development of new resources? Could some of the required resources be developed during the course of another project? Opportunities should always be gauged against your goals. When presented with multiple potential projects, you should identify which will progress us further towards our goal. Not every potential project that comes your way is an opportunity to make progress towards those goals. For instance, some projects will place heavy demands on your time and energy, holding back your internal resource development. You should also gauge these opportunities against your resources. Candidate development projects should be checked off against available tools, techniques, and workflows. True opportunities must also be achievable. Some projects will present significant risk of failure, as they draw on resources that are either yet to be developed, or that are not yet fully tested. Bottom line, a project must return a profit. It is a fact of life that to be sustainable, any activity requires funding. Sometimes, this will mean that you have to turn down a project; simply because the time required to carry it out will cost more than the income the project will generate. In this section I will suggest some considerations that may help you identify true opportunities.
23.2.1
Speculative Developments
The ArchiCAD community is hungry for software products that enhance the process of design and documentation. Developing software for this global market can return a good level of income. When developing speculatively, you as a programmer are in control of two key factors: efficient development, and key benefits.
Chapter 23: Development Strategies - Opportunity 419 Efficient development involves choosing projects that fit well with existing resources. Any project that requires the creation of new resources is likely to be inefficient. Admittedly, speculative development is the one type of development that could directly pay for the development of new resources. Because the income is related directly to the perceived benefits of the product, the smarter and easier to use it is the more units you will sell, and the more profit you will make. Also, the only time limit on development is self-imposed. Thus it would seem to make sense to include some resource development within the budget of the project. I believe that this approach, even in speculative development, is very risky. Later, we will consider a more managed approach to resource development which has less attached risk, and allows us to use a single set of rules to identify opportunity in both commissioned and speculative development. Efficient development does not necessarily equate to delivering a product quickly. In speculative developments, the support overhead is multiplied by the sheer number of users. Support can become overwhelming, as more and more people start using the product. To avoid having to provide excessive support, it is important that the product has been thoroughly tested, and that the user interface is as intuitive as possible. Key benefits are what will motivate an ArchiCAD user to purchase the product. Two main categories of key benefits are productivity and design. During the intial stages of design, Architects want to design freely without restrictions that may be imposed by the CAD software. The second stage of the design process deals with documentation. Throughout the design and documentation process, and for the construction project as a whole, greater efficiency translates to greater profits. If the key benefits of a product are exceptional, the balance may tip the other way, and the product may help motivate someone to purchase ArchiCAD.
23.2.2
Pro-Active Development
Rather than take on a project with the risk of failure, a percentage (perhaps 20%) of your time as a programmer should be dedicated to producing new resources, with an eye to specific future developments. Using this approach I will define an opportunity as a project for which you possess all the required re-usable resources, and have the staff resources to deliver the product in the required time frame. You should consider the nature of the re-usable resources you will create. Will they in turn lead to further opportunity, or will they be seldom used? Some guidelines you might like to consider include: • Develop new resources with an eye to future projects. • Thoroughly test all resources before using them. • Keep a list all the re-usable resources that you have, and that have been thoroughly tested. This is your re-usable resource pool. The concept of resource is far wider ranging than mere fragments of programming code. All of the resources you need for programming need to be developed. This might include, for example, learning some mathematical strategies for solving
420 Chapter 23: Development Strategies - Opportunity problems, reading a book like this one, or taking a course that injects some new ideas. It also includes your stock of icons, internal documentation and standards, and external resources.
23.2.3
Required Resources
In order to identify an opportunity, you should list all of the resources you will need. To do this, the scope and nature of the project must be well defined. • Define the exact nature and scope of the project as accurately as possible. • List the re-usable resources required. Next, you should consider your existing re-usable resource pool. Do you currently possess all of the necessary resources for the project? If you need to develop new resources in order to complete the project, this is not yet an opportunity but rather a risk. If you want to proceed, you should develop the resources first: only then should you return to consider the project’s potential as an opportunity.
23.2.4
Factor in the Time
You should consider the non-reusable resources (time and budget) that will be expended. In particular you should answer the question, how long will the project take? • Factor in the development time. • Factor in the ongoing support load.
23.2.5
Estimate the Income
The biggest question of all is that of potential income. Will the investment in time and resource result in a profit or a loss? What will be the magnitude of this return? Remember also that you will have to somehow bring your product to the market.
23.2.6
Is it an Opportunity?
Previously we defined an opportunity as a project for which you possess all the required re-usable resources, and have the staff resources to deliver the product in the required time frame. We should add that the best opportunities are those that maximize the return while minimizing the expense. Re-usable resources are only useful if they are in fact re-used. They should be used as frequently as possible. New re-usable resources should be added to the pool, either during the course of a project (a risky approach), or preferably in a proactive way. Thus, when presented with more than one opportunity, you can choose the one that provides the maximum income for the minimum expense. By constantly making improvements to the resource pool, identifying better opportunities, and programming more
Chapter 23: Development Strategies - Some Thoughts on Planning and Design 421 efficiently, it is possible to produce better quality applications more efficiently.
23.3 Some Thoughts on Planning and Design Whatever project you take on, however big or small, you will want to produce a great product that stands the test of time. There are a variety of motivations for such a goal. On the one hand, you have your job satisfaction and reputation to consider. There are few things more soul-destroying than working on a poorly conceived project that you know will never satisfy the client’s needs, and conversely it can be very fulfilling to successfully build a solution that can immediately be useful to your client. Then again, a list of satisfied customers can provide valuable references for your future business, and almost certainly repeat custom. Extra to these considerations looms the dark specter of change. As your client’s business grows the requirements his business puts on your product will change. This change might take place over a short or a long period of time, but it will certainly happen. It can be tempting to view a development project in isolation, as if successful completion is the end of the job. But it is never like that. What are the chances that a solution created in 2012 will still be relevant in 2042? Almost zero. At some point your product will need to be extended to cope with the changing demands. You should always take this into consideration and design an inherently extensible structure into your software. Having briefly touched on the goals of design, we will consider some of the obstacles to achieving these goals, and finally develop a paradigm that may help you succeed where others have failed.
23.3.1
Two Obstacles to Success
However skilful you are at programming, there are two major hurdles to overcome when you are faced with a new project. These hurdles are communication with the client and a limited budget. Communication In many cases limited communication at the start of the project will result in your having a somewhat shallow understanding of the client’s requirements. If you are developing for a specific client, the client will have a deep understanding of the product requirements, but will be reluctant to expend the hours of time to meticulously spell out how the existing system works in every detail. Often the person with whom you are liaising will know only a part of the processes involved, and will not understand the practical or historical reasons that have shaped the way their company approaches the day-to-day tasks they require your product to automate. If you are developing for a global clientele, each locality will have its own building regulations, and each client will have slightly different requirements. This poses problems in interface design, as a generic product must necessarily be designed to cope with a greater range of input. You may also want to allow the product to be modified for use internationally, so at
422 Chapter 23: Development Strategies - Some Thoughts on Planning and Design the very least you will need a strategy to allow for a variety of spoken languages. This problem in communication is inevitable, and while every effort is made to communicate effectively, it is very rare that a complete exchange of all salient information can occur before the project is begun. The client will be spending a lot of money for a custom solution, not so much for an out-of-the-box software package. Either way, they will expect to get at least what they paid for, and usually more. Generally the end-user knows only that they expect to be working more efficiently, more accurately, or with fewer restrictions after the product is installed than they were before. In other words, they expect to be reaping a tangible benefit from their investment, and that benefit should be the one they expected. If a client asks for software that will make their processes more efficient, it’s no point giving them something that is as efficient, or that is more efficient only in some cases. Nor will the client appreciate how much time and expertise is required to achieve the solution they desire. Limited Budget You will always be working on a limited budget. If you are developing products for the mass market, your budget will be based on how many units you expect to sell, and the price per unit. If you are working for a specific client, the budget will be driven externally. When you present an estimate for the full cost of the work the client is almost certain to balk. The first reaction will be to try to reduce the cost of the project by cutting back the scope of the work, slashing the cost for certain items, or even requesting that work be carried out at no cost.
23.3.2
Strategies for Designing a Successful Product
So how can you possibly produce a product that will satisfy the end users? When you begin to solve a Sudoku or a crossword puzzle, all of the information you need to solve the puzzle is right there in front of your eyes. However, it is not possible to see the full solution directly. Each solved clue provides more information that can help to solve other clues. Rather than planning a complete solution, we rather apply a set of strategies that have proven successful in winning similar games. The same is true of software development. In this section I will propose a set of strategies for development that I have found to be helpful in designing and implementing software that can pass the test of time. Think Big Imagine what your product will be like in ten years time. Will it still be in use? Will it be intuitive and easy to use? When a client requests an enhancement, will you be able to deliver on that request, and in what timeframe? Plan for a product that can grow from small beginnings to a great product with every base covered.
Chapter 23: Development Strategies - Some Thoughts on Planning and Design 423 Plan Meticulously Put your plans through vigorous tests. Think of what features different users might expect of the product, and how they would expect it to work. Are your plans big enough to accommodate these requirements? Plan for Customization When you think of a customizable solution, you tend to imagine a generic product that is distributed globally to thousands of clients. Each client has a rather unique approach to his or her own brand of design and documentation. The ability to tailor a product is obvious in such cases. However, customizability can be just as important for single-client products. Often the client’s requirements can change over time. A timber framing solution may need to allow for new timber grades or sizes, or the company using it might take on a steel framing product. Implement in Stages Rather than creating a complete, finished product in one shot, develop the project incrementally over time. Identify key must-have features within the framework of the big picture, and implement those features first. Ask the question of your client, or poll the market to get an understanding of what product features are indispensible. Often you can identify a set of functions that will make your product facilitate one process that your client is struggling with. Manage the Resource Requirements Plan your project as far as possible in such a way that all essential functionality can be achieved using existing resources and technology. Avoid situations in which you need to develop brand new resources as part of a project. Any required resource development should be carried out in allocated research time, and the success or failure of a current project should not depend on the outcome of that research. Research is intrinsically experimental, and the outcomes cannot be confidently predicted. Even if you are convinced in your mind that a certain process or algorithm can be successfully designed and implemented, there is no way of knowing how long it might take to arrive at a rock-solid solution. It would be foolish to stake the success of a project on the results of such research. I know how tempting it can be to build the cost of research into a project, but in reality this is neither ethical nor smart. It leads to gambling with your own or your client’s money, often in sizeable amounts. This of course can be very exciting and quite addictive, but is doomed to end in tears.
23.3.3
The Master Plan
I believe the overriding strategy to planning is to think big, and implement in stages. The idea is that each stage of implementation should introduce benefits for the end user. Feedback from each stage will decide the focus of the next stage, and may change the big picture. The smaller the increment, the lesser the risk. The bigger the picture, the lower the probability that you will be forced to revise it.
424 Chapter 23: Development Strategies - Improving the Resource Pool Thinking big means thinking of all the possible uses to which your application might be put, and saying yes, that will be catered for by the final product. It might take ten years before your product is complete. But you should plan your big picture product so that when you implement each stage the whole picture slowly takes shape. Planning this way might seem like overkill if your sights are set only on completing a one-off project for a single client. But if you take into account the fact that your client may be using the software you produce for the next twenty to thirty years or more, chances are that he will eventually request all of the features you can imagine when you initially design the product, plus many more. I once worked on a project that, in hindsight, was destined to failure from day one. The client wanted a library of GDL objects to produce documentation for a hospital. The project was time-intensive, but by deliberately restricting the scope of the project and limiting the functionality, we were able to price it within the client’s budget. This secured us the contract but left us open to a world of hurt. As it turned out, the client was happy with the restricted scope when it came to project costs, but not when it came to daily use of the product. The people who paid the bill were not the same people who used the software on a daily basis. While we successfully provided a minimal solution within the budgeted costs, the inherent shortcomings in the project’s design were experienced by the end users not as a cost saving but as a design flaw. If we were to take on a similar project in the future, the better approach would be to sell the concept of a growing product to the client. This is always a win-win situation. The risk for both parties is negligible. As each small step towards a successful outcome is achieved, each party gains a deeper understanding of how the other operates. With good management on both sides, a working relationship can be formed, or either party can opt out of future development. Poorly conceived products will always come back to bite you. Generally this is because it is hard to bolt functionality onto a product that was never designed to include that functionality. It’s like putting a 3.9 liter V8 engine into a Fiat Bambino. When sufficient torque is generated the car will simply fall to pieces, or become impossible to handle. To achieve success, you would have to modify nearly all the mechanical components of the car. You would be left with essentially a new car with the Bambino body the only original part. For the investment you made, you would almost certainly have been better to buy a BMW Z9 – at least it would look the part. In everyday personal life, the software equivalent of a Bambino may be an acceptable ride. Clients who spend thousands of dollars for a tool they use to generate an income are likely to be more demanding.
23.4 Improving the Resource Pool The concept of resource is very generic. It encompasses pretty much everything you can use when developing. Obviously the most important resource you possess is yourself – your intelligence, skills, intuition and judgment are unique. Without them, development would be impossible. Software resources include any add-ons, macros or script fragments you can obtain, either by inheritance, purchase or through your own work. Other resources include icons, systems and standards that you have built up.
Chapter 23: Development Strategies - Improving the Resource Pool 425 The more and better the resources you have at your command, the more easily and efficiently you will be able to use them to develop applications. In this section we will look at strategies that you can use to optimize the resources at your disposal
23.4.1
Personal Resources
The most important resource you have is yourself. Your personal drive, interests, intelligence, experience, mental skills and motor skills, make you the GDL programmer that you are. So how can you improve on the quality and range of your personal resources? The answer to this question is timeless. In ancient times, it was inscribed in gold letters at the lintel of the entrance to the temple of Apollo at Delphi. In two simple words, γνώθι σεαυτόν (usually translated as ‘know thyself’). Even before you write your first GDL script, you already possess skills in a number of areas that will be useful to you as a GDL programmer. Obviously, no-one has all the talents and time to become the perfect programmer. But you are unique, and that is your strength. It is simply a matter of developing your own personal strengths and improving any areas of weakness. By identifying your personal strengths and weaknesses, you are already on the road to improving those strengths, and working on those weaknesses. Mathematical skills, including techniques learned in geometry, trigonometry and calculus will be in constant use. GDL is fundamentally geometric in nature. In order to use it, you must possess a basic proficiency in geometry and trigonometry. The better your grasp on these topics, the more natural you will find the terminology and application of GDL programming. Problem solving in one form or another will constantly present itself. How best to display data on a user interface palette, how to model complex 3D forms, the best format in which to store data, and countless other problems are the bread and butter of the GDL programmer. Listening skills, self analysis and patience are essential resources for the GDL programmer. Often there are many ways to achieve the same end. You may occasionally think of the best solution immediately, but often input from other sources will uncover a better approach. It is important to be open to constructive criticism, and to read between the lines to turn negative criticism into something positive that you can work towards. Other skills you may have to work on include script formatting and the use of the various GDL commands. It is important to understand not only how to use the individual commands, but also how to choose appropriate commands to achieve specific results, and how to select the best options for the specific project under development. Some of this can only be learned from experience. Everyone comes into GDL programming with a unique skill set. Some may be strong in mathematics and relatively weak artistically. Others may find mathematics challenging. Some will have strengths across a wide variety of skills. But whatever skills you have, they can always be extended, improved, and optimized, and new skills can be acquired. It is important to have the best skill set possible. It is possible to create a canoe using a stone adze, but with the accumulated
426 Chapter 23: Development Strategies - Implementation skills and technology of the past few thousand years it is also possible to create a supersonic airplane. The same is true of GDL (albeit on a somewhat shorter timeline). With better tools and resources, you can create superior objects and libraries. The more you know the more you will be able to apply your knowledge. To improve your knowledge and skills requires conscious effort. Sometimes knowledge comes by chance, but usually you have to search for it. You will need to source information, solve problems, practice techniques and develop strategies. A course might provide opportunities to absorb information efficiently. You may have to visit the local library, or browse the internet. Nowadays, the internet is a great source of information. In other words, you must be prepared to constantly learn from any available source. With knowledge, of course, you need understanding. It’s one thing being able to recite the cosine rule and another thing understanding when to use it and what potential errors may arise.
23.4.2
Software Tools
New software and other resources are often created during the course of research and development, but can also be obtained from other sources. Existing software resources can be improved. Tools that are deficient, either in their scope or in the way they perform certain tasks, should be re-developed or fixed. This includes not only whether they accomplish what they set out to do, but also how efficiently and how accurately they perform their assigned tasks. Develop generic, re-usable resources whenever possible. When designing resources for a specific application, think about how they could be made more generally useful for other applications. If you design your resources to be generic, you can re-use them without modification, saving development time later on. Also, any bug fixing or enhancement done to a generic resource will apply to all applications that make use of it.
23.4.3
Scheduled Resource Development
The need for a particular resource often becomes apparent in the context of a new potential development. While planning the development, it becomes apparent that the resource will be required. As we discussed earlier, it is risky to start a project before all resources are available. This leads naturally to the concept of scheduling time for resource development. This time is not directly paid for by any one project, so should be carefully budgeted for. While a resource is in development, and until it has been carefully tested, other projects that do not rely on the resource can progress.
23.5 Implementation Implementation is the process of converting opportunity and resource to create an application. This is the truly creative
Chapter 24: Example Objects - Testing 427 part of a programmer’s job. Everything else (planning, resource development, testing etc.) supports the process of implementing software. However, even during the implementation stage, thought must be given to how it fits into the bigger picture. Always use a well structured and readable scripting style so that debugging and maintenance can be carried out efficiently. Including re-usable resources will speed implementation, ensure dependability, and reduce maintenance costs. Implement the core functionality first. Any extra functionality can be left to the future, providing that the product has been well designed. Resist the temptation to add extra functionality at the last moment.
23.6 Testing Testing should be carried out as you implement your products. Whenever you add a new function, test it in isolation then explore the system to see whether the changes have caused issues in other functions of the product. The goal of testing is to push the product to failure. Early detection is the key, especially if you discover a bug before moving on to a new development task. When a bug is found, fix it immediately and check that the fix actually works.
23.7 Maintenance Finally, a word about maintenance. Every software product needs maintenance. On the one hand, bugs will be found and must be fixed. Then again, there is always scope to extend or improve a product. Whenever a new upgrade of ArchiCAD is released, it is possible that changes will affect your product. When budgeting time for a project, you should also build in time for ongoing maintenance. This also applies to what you charge the client, and how you charge. It is unrealistic to imagine that you can do a one-off job for a client and then forget about it. Software is a living thing. As a developer, the most enjoyable input into a product’s life-cycle is at the implementation stage, but you should understand that at the point of delivery you have assumed full responsibility for your creation. A well-designed product will not give you a lot of grief, as it can be grown and developed over time. Poorly designed and implemented products will cause endless problems and in the end will have to be re-designed. To fund this ongoing maintenance, you could charge clients an annual fee. As you create more products, the annual maintenance load will increase, making you less productive. This is of course a sign of success, and the solution may well be as simple as hiring and training more staff.
24 Example Objects In this final chapter I have included a few examples to illustrate how some of the techniques discussed in the book can be used in practice. Generally, each object you create will draw on a variety of resources. Only when you have some appreciation of what resources are available can you start to make decisions on how to design and script library parts.
24.1 Playground Equipment/Barrel Bridge The first object comes from a collection of Playground Equipment. We won’t consider either the Interface Script or the 2D Script in this example. Template Object A template object was first constructed with the parameters illustrated here. This template object is intended for use by a collection of library parts that together form the components of an adventure playground for children. It includes parameters that are common across many of the library parts in this collection (figure 362). Extra Parameters The barrel bridge object requires a few extra parameters. To define the steel bands connecting the boards to form a cylinder, add lengthtype parameters bandSpacing bandwidth, bandThick and bandOffs, and the material-type parameter bandMat. The 3D Script The 3D Script is given below. It makes use of the tube and prism elements, some math to calculate the exact number and positions of the boards, and a loop to place the steel bands and timber boards. !Curve Tracing toler .001 !Constants tol = .00001
Figure 362 – The template object ‘Playground Equipment’ object contains a bunch of parameters that can be used by objects in this collection. Specific parameters will still have to be added to the individual objects in the collection.
Chapter 24: Example Objects - Playground Equipment/Barrel Bridge 429 R = zzyzx/2 - beamDepth qi = asn((boardWidth/2)/R) R = R*cos(qi) - .001 L = 2*pi*R nBands = max(2, int(2 + (a - 2*bandOffs - bandWidth)/bandSpacing)) !Pen pen gs_cont_pen
3D Hotspots are placed on the bounding box. !Hotspots on the Bounding Box hotspot 0, 0, 0 hotspot a, 0, 0 hotspot 0, b, 0 hotspot a, b, 0 hotspot 0, 0, zzyzx hotspot a, 0, zzyzx hotspot 0, b, zzyzx hotspot a, b, zzyzx
The beams used to support the barrel structure are included with the object. Each beam is modeled via a call to a subroutine, that draws the beam and applies texture mapping. !Beams !Bottom Left gosub “Beam” !Bottom Right addx a: mulx - 1 gosub “Beam” del 2 !Top Left addz zzyzx – beamDepth gosub “Beam” del 1 !Top Right add a, 0, zzyzx – beamDepth: mulx - 1 gosub “Beam” del 2
The boards are defined using a loop. Each iteration places one board at an incremental angle.
430 Chapter 24: Example Objects - Playground Equipment/Barrel Bridge !Boards material timberMat nBoards = int(L/(boardWidth + boardGap)) LGap = L - nBoards*boardWidth boardGap = LGap/nBoards boardSpacing = boardWidth + boardGap add 0, b/2, zzyzx/2 for xi = 0 to L step boardSpacing qi = (xi/R)*(180/pi) body -1 rotx qi: addz -R prism 4, boardThick, 0, -boardWidth/2, a, -boardWidth/2, a, boardWidth/2, 0, boardWidth/2 base vert 0, 0, 0 vert .001, 0, 0 vert 0, .001, 0 vert 0, 0, .001 coor 2, -1, -2, -3, -4 body -1 del 2 next xi del 1
The steel hoops to which the boards are fixed are modeled using prisms. !Steel Bands material bandMat add 0, b/2, beamDepth bandSpacing = (a - 2*bandOffs - bandWidth)/(nBands - 1) for i = 1 to nBands xform 0, 0, 1, bandOffs + (i - 1)*bandSpacing, 1, 0, 0, 0, 0, 1, 0, 0 prism_ 3, bandWidth,
Chapter 24: Example Objects - Playground Equipment/Swing 431 0, zzyzx/2 - beamDepth, 979, zzyzx/2 - beamDepth + bandThick, 360, 4079, zzyzx/2 - beamDepth, 360, 4079 del 1 next i del 1 end
By using a sub-routine to model the beam, the complexity of texture mapping is removed from the main script where it would otherwise have to be repeated four times. Another benefit is that the beam definition can be later extended to include other components (eg. bolts and fixing plates) without having to edit the main script. “Beam”: material timberMat body -1 prism 4, beamDepth, 0, 0, beamThick, 0, beamThick, b, 0, b base vert 0, 0, 0 vert .001, 0, 0 vert 0, .001, 0
Figure 363 – Run the 3D Script to view the barrel bridge model.
vert 0, 0, .001 coor 2, -1, -3, -2, -4 body -1 return
Before attempting to run or save this script, you should set ‘sensible’ defaults for all the parameters. Otherwise there is a distinct danger that the 3D Script may enter an infinite loop.
24.2 Playground Equipment/Swing This object uses values lists, GDL primitives, Bezier splines, continuous tubes, loops, and stack operations, The extra parameters (on top of the parameter set inherited from the Playground Equipment template) are as follows. To give control on the overall structure, integer-type parameters cableType and swingType were added. To control the
432 Chapter 24: Example Objects - Playground Equipment/Swing swing seat I added length-type parameters seatLength, seatWidth, and seatDroop. To define the chain links I included length-type parameters linkLength, linkWidth and linkThick. Master Script The Master Script reads as follows. tol = .0001
Parameter Script The Parameter Script is given next. This script defines values lists for the cableType and swingType parameters, and for the page selection parameter. values "page" 1, 2, 3, 4 values "swingType" 1, 2, 3 values "cableType" 1, 2
3D Script The 3D Script uses a range of techniques. !Pen pen gs_cont_pen !Hotspots hotspot 0, 0, zzyzx hotspot a, 0, zzyzx hotspot a/2, 0, zzyzx hotspot (a - seatLength)/2, 0, 0 hotspot (a + seatLength)/2, 0, 0 !Declare Arrays dim tList[], LList[], linkX[][], linkY[][], linkZ[][] !Constants linkRadius = (linkWidth - linkThick)/2 connectorHeight = .060 connectorWidth = .040 connectorThick = .008
The chain link is modeled once and stored in a group. !A Single Link for a Chain Type Cable if cableType = 1 then group "Link"
Chapter 24: Example Objects - Playground Equipment/Swing 433 resol 5 material cableMat Ri = linkRadius + linkThick/2 nq = 12 dq = 180/nq for qi = 0 to 180 + dq/2 step dq put linkRadius - Ri*sin(qi), Ri*cos(qi), 0, 0 next qi for qi = 0 to 180 + dq/2 step dq put linkLength - linkRadius + Ri*sin(qi), -Ri*cos(qi), 0, 0 next qi for qi = 0 to 40 + dq/2 step dq put linkRadius - Ri*sin(qi), Ri*cos(qi), 0, 0 next qi tube 2, nsp/4, 3, 0, 0, 901, linkThick/2, 360, 4001, get(nsp) endgroup endif
Cables are defined either as ropes or chains via sub-routines. !Cables !Left-hand Cable x1 = 0 y1 = 0 z1 = zzyzx - connectorHeight x2 = (a - seatLength)/4 y2 = 0 z2 = (zzyzx - connectorHeight)*.4 x3 = (a - seatLength)/2 y3 = 0 z3 = 0 add x1, y1, zzyzx gosub "Connector" del 1 if cableType = 1 then
434 Chapter 24: Example Objects - Playground Equipment/Swing gosub "Chain" endif if cableType = 2 then z1 = z1 - connectorThick z3 = z3 + connectorThick gosub "Rope" endif !Right-hand Cable x1 = a y1 = 0 z1 = zzyzx - connectorHeight x2 = (3*a + seatLength)/4 y2 = 0 z2 = (zzyzx - connectorHeight)*.4 x3 = (a + seatLength)/2 y3 = 0 z3 = 0 add x1, y1, zzyzx gosub "Connector" del 1 if cableType = 1 then gosub "Chain" endif if cableType = 2 then z1 = z1 - connectorThick z3 = z3 + connectorThick gosub "Rope" endif
The seat is also defined within a sub-routine. It could have been defined within the main program, as it is only used once. !Seat gosub "Seat" end
A sub-routine to model a complete chain formed used by the chain link previously defined. "Chain": !Calculate dx, dy, dz for the chain dx = x3 - x1
Chapter 24: Example Objects - Playground Equipment/Swing 435 dy = y3 - y1 dz = 0 !Calculate an approximate chain length L = sqr((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2) L = L + sqr((x3 - x2)^2 + (y3 - y2)^2 + (z3 - z2)^2) !Calculate a step size for t that will provide a smooth path !for the chain links nt = (1 + 3*int(L/linkLength)) dt = 1/nt !Calculate the chain lengths for each value of t i = 1 tList[i] = 0 LList[i] = 0 xPrev = x1 yPrev = y1 zPrev = z1 for t = dt to 1 + dt/2 step dt s = 1 - t xi = x1*s^2 + 2*x2*s*t + x3*t^2 yi = y1*s^2 + 2*y2*s*t + y3*t^2 zi = z1*s^2 + 2*z2*s*t + z3*t^2 i = i + 1 tList[i] = t LList[i] = LList[i - 1] + sqr((xi - xPrev)^2 + (yi - yPrev)^2 + (zi - zPrev)^2) xPrev = xi yPrev = yi zPrev = zi next t L = LList[nt + 1] !Calculate number of links and link spacing nLinks = int(1 + L/linkLength) linkSpacing = L/nLinks !For each link, find the start & end point dL = (linkLength - linkSpacing)/2 linkSlip = sqr(linkRadius^2 - (linkRadius - 2*dL)^2) q = 0
436 Chapter 24: Example Objects - Playground Equipment/Swing for i = 1 to nLinks !Interpolate the value of t corresponding with !the link start point linkL0 = (i - 1)*linkSpacing - dL linkStartPointFound = 0 j = 0 repeat j = j + 1 if LList[j] > linkL0 then linkStartPointFound = j endif until j > nt or linkStartPointFound j = linkStartPointFound - 1 if j = 0 then linkStartT = 0 else linkStartT = tList[j] + (tList[j + 1] - tList[j])*(linkL0 - LList[j])/(LList[j + 1] - LList[j]) endif !Find the link start point t = linkStartT s = 1 - t linkX[i][1] = x1*s^2 + 2*x2*s*t + x3*t^2 linkY[i][1] = y1*s^2 + 2*y2*s*t + y3*t^2 linkZ[i][1] = z1*s^2 + 2*z2*s*t + z3*t^2 !Interpolate the value of t corresponding with !the link start point linkL1 = linkL0 + linkLength linkEndPointFound = 0 j = linkStartPointFound - 1 repeat j = j + 1 if LList[j] > linkL1 then linkEndPointFound = j endif until j > nt or linkEndPointFound if linkEndPointFound = 0 then
Chapter 24: Example Objects - Playground Equipment/Swing 437 linkEndPointFound = nt + 1 endif j = linkEndPointFound - 1 if j = nt then linkEndT = 1 else linkEndT = tList[j] + (tList[j + 1] - tList[j])*(linkL1 - LList[j])/(LList[j + 1] - LList[j]) endif !Find the link end point t = linkEndT s = 1 - t linkX[i][2] = x1*s^2 + 2*x2*s*t + x3*t^2 linkY[i][2] = y1*s^2 + 2*y2*s*t + y3*t^2 linkZ[i][2] = z1*s^2 + 2*z2*s*t + z3*t^2 !Find the direction vector for the link (local 'x') ux = linkX[i][2] - linkX[i][1] uy = linkY[i][2] - linkY[i][1] uz = linkZ[i][2] - linkZ[i][1] Li = sqr(ux^2 + uy^2 + uz^2) ux = ux/Li uy = uy/Li uz = uz/Li !Find the local 'y' for the link vx = dy*uz - dz*uy vy = dz*ux - dx*uz vz = dx*uy - dy*ux Li = sqr(vx^2 + vy^2 + vz^2) vx = vx/Li vy = vy/Li vz = vz/Li !Find the local 'z' for the link wx = uy*vz - uz*vy wy = uz*vx - ux*vz wz = ux*vy - uy*vx Li = sqr(wx^2 + wy^2 + wz^2) wx = wx/Li
438 Chapter 24: Example Objects - Playground Equipment/Swing wy = wy/Li wz = wz/Li !Draw the link xform ux, vx, wx, linkX[i][1], uy, vy, wy, linkY[i][1], uz, vz, wz, linkZ[i][1] q = q + 15 - rnd(35) if i%2 then rotx q else rotx 90 + q endif addz linkSlip*(1 - rnd(2)) placegroup "Link" del 3 next i return
A sub-routine to define a rope (this has fewer polygons than a chain). "Rope": resol 6 nt = 6 dt = 1/nt for t = -dt to 1 + dt + dt/2 step dt s = 1 - t put x1*s^2 + 2*x2*s*t + x3*t^2, y1*s^2 + 2*y2*s*t + y3*t^2, z1*s^2 + 2*z2*s*t + z3*t^2, 0 next t tube 2, nsp/4, 3, 0, 0, 901, cableThick/2, 360, 4001, get(nsp) return "Connector": resol 6
Chapter 24: Example Objects - Playground Equipment/Swing 439 material cableMat Ri = (connectorWidth + connectorThick)/2 put -Ri, 0, 1, 0, -Ri, 0, -connectorHeight/4, 0 for qi = 0 to 180 step 20 put -Ri*cos(qi), 0, -connectorHeight + connectorWidth/2 - Ri*sin(qi), 0 next qi put Ri, 0, -connectorHeight/4, 0, Ri, 0, 1, 0 tube 2, nsp/4, 51, 0, 0, 901, connectorThick/2, 360, 4001, get(nsp) mulz -1 prism 4, connectorHeight/2, -connectorWidth/2 + tol, -connectorThick, connectorWidth/2 - tol, -connectorThick, connectorWidth/2 - tol, connectorThick, -connectorWidth/2 + tol, connectorThick del 1 return
Options exist for the seat. It can either be a flexible strap, a belted infant seat, or a simple slab of wood, The first option is modeled using a tube and a Bezier spline. The infant seat is modeled using GDL primitives. The wooden slab is modeled using a prism. "Seat": material seatMat !Simple Seat if swingType = 1 then x1 = (a - seatLength)/2 y1 = 0 z1 = -linkThick x2 = a/2 y2 = 0 z2 = -linkThick - 2*seatDroop x3 = (a + seatLength)/2 y3 = 0
440 Chapter 24: Example Objects - Playground Equipment/Swing z3 = -linkThick nt = 12 dt = 1/nt for t = -dt to 1 + dt + dt/2 step dt s = 1 - t xi = x1*s^2 + 2*x2*s*t + x3*t^2 yi = y1*s^2 + 2*y2*s*t + y3*t^2 zi = z1*s^2 + 2*z2*s*t + z3*t^2 put xi, yi, zi, 0 next t tube 4, nsp/4, 51, -seatWidth/2, 0, 1, seatWidth/2, 0, 1, seatWidth/2, -.003, 1, -seatWidth/2, -.003, 1, get(nsp) endif !Infant Seat if swingType = 2 then rotx 0 if cableType = 1 then add (a - seatLength)/2, .025, 0!.025 endif if cableType = 2 then add (a - seatLength)/2, .025, -linkThick!.025 endif nt = 10 dt = 1/nt dim spineX[][], spineY[][], spineZ[][] !Top Band put 0, 0, 0, 0, -.7*seatLength, 0, seatLength/2, -.7*seatLength, 0, seatLength, -.7*seatLength, 0, seatLength, 0, 0 gosub "Quartic Spine"
Chapter 24: Example Objects - Playground Equipment/Swing 441 put 0, 0, -.050, 0, -.7*seatLength, -.050, seatLength/2, -.7*seatLength, -.050, seatLength, -.7*seatLength, -.050, seatLength, 0, -.050 gosub "Quartic Spine" !Bottom Band put .01, -.050, 0, .01, -.050, -.7*seatLength, seatLength/2, -.050, -.7*seatLength, seatLength - .01, -.050, -.7*seatLength, seatLength - .01, -.050, 0 gosub "Quartic Spine" put .005, 0, 0, .005, 0, -.7*seatLength, seatLength/2, 0, -.7*seatLength, seatLength - .005, 0, -.7*seatLength, seatLength - .005, 0, 0 gosub "Quartic Spine" !Mid Band put seatLength/2 - .06, -.05 - .8*(seatLength - .05)/2, -.05 - .8*(seatLength - .05)/2, seatLength/2 - .03, -.05 - .8*(seatLength - .05)/2, -.05 - .8*(seatLength - .05)/2, seatLength/2, -.05 - .8*(seatLength - .05)/2, -.05 - .8*(seatLength - .05)/2, seatLength/2 + .03, -.05 - .8*(seatLength - .05)/2, -.05 - .8*(seatLength - .05)/2, seatLength/2 + .06, -.05 - .8*(seatLength - .05)/2, -.05 - .8*(seatLength - .05)/2 gosub "Quartic Spine" !Ribs dim ribX[][], ribY[][], ribZ[][] for i = (spineN + 1)/2 - 2 to (spineN + 1)/2 + 2 put spineX[2][i], spineY[2][i], spineZ[2][i], spineX[5][i], spineY[5][i], spineZ[5][i],
442 Chapter 24: Example Objects - Playground Equipment/Swing spineX[3][i], spineY[3][i], spineZ[3][i] gosub "Quadratic Rib" next i body -1 base !Vertices for i = 1 to 4 for j = 1 to spineN vert spineX[i][j], spineY[i][j], spineZ[i][j] next j next i for i = 1 to nRibs for j = 1 to ribN vert ribX[i][j], ribY[i][j], ribZ[i][j] next j next i !Edges !Top Band !Horizontals for i = 1 to 2 for j = 1 to spineN - 1 if i = 2 and j > (spineN + 1)/2 - 3 and j < (spineN + 1)/2 + 2 then s = 1 else s = 0 endif edge spineN*(i - 1) + j, spineN*(i - 1) + j + 1, -1, -1, s next j next i !Verticals for j = 1 to spineN edge j, spineN + j, -1, -1, 2*(j > 1 and j < spineN) next j !Diagonals for j = 1 to spineN - 1 edge j, spineN + j + 1, -1, -1, 2
Chapter 24: Example Objects - Playground Equipment/Swing 443 next j !Bottom Band !Horizontals for i = 3 to 4 for j = 1 to spineN - 1 if i = 3 and j > (spineN + 1)/2 - 3 and j < (spineN + 1)/2 + 2 then s = 1 else s = 0 endif edge spineN*(i - 1) + j, spineN*(i - 1) + j + 1, -1, -1, s next j next i !Verticals for j = 1 to spineN edge 2*spineN + j, 3*spineN + j, -1, -1, 2*(j > 1 and j < spineN) next j !Diagonals for j = 1 to spineN - 1 edge 2*spineN + j, 3*spineN + j + 1, -1, -1, 2 next j !Crotch Band !Horizontals for i = 1 to nRibs firstVert = 4*spineN + (i - 1)*ribN for j = 1 to ribN - 1 s = 0 if i > 1 and i < nRibs then s = 2 endif edge firstVert + j, firstVert + j + 1, -1, -1, s next j next i !Verticals for i = 1 to nRibs - 1
444 Chapter 24: Example Objects - Playground Equipment/Swing firstVert = 4*spineN + (i - 1)*ribN for j = 1 to ribN edge firstVert + j, firstVert + ribN + j, -1, -1, 2 - 1*(j = 1 or j = ribN) next j next i !Diagonals for i = 1 to nRibs - 1 firstVert = 4*spineN + (i - 1)*ribN for j = 1 to ribN - 1 edge firstVert + j, firstVert + ribN + j + 1, -1, -1, 2 next j next i !Polygons !Top Band firstHorz = 0 firstVert = firstHorz + 2*(spineN - 1) firstDiag = firstVert + spineN for j = 1 to spineN - 1 pgon 3, 0, 2, firstHorz + j, firstVert + j + 1, -(firstDiag + j) pgon 3, 0, 2, -(firstHorz + (spineN - 1) + j), -(firstVert + j), firstDiag + j next j !Bottom Band firstHorz = firstDiag + (spineN - 1) firstVert = firstHorz + 2*(spineN - 1) firstDiag = firstVert + spineN for j = 1 to spineN - 1 pgon 3, 0, 2, firstHorz + j, firstVert + j + 1, -(firstDiag + j) pgon 3, 0, 2, -(firstHorz + (spineN - 1) + j), -(firstVert + j), firstDiag + j next j
Chapter 24: Example Objects - Playground Equipment/Swing 445 !Crotch Band firstEdge = firstDiag + (spineN - 1) for i = 1 to nRibs - 1 firstHorz = firstEdge + (i - 1)*(ribN - 1) firstVert = firstEdge + nRibs*(ribN - 1) + (i - 1)*ribN firstDiag = firstEdge + nRibs*(ribN - 1) + (nRibs - 1)*ribN + (i - 1)*(ribN - 1) for j = 1 to ribN - 1 pgon 3, 0, 2, firstHorz + j, firstVert + j + 1, -(firstDiag + j) pgon 3, 0, 2, -(firstHorz + (spineN - 1) + j), -(firstVert + j), firstDiag + j next j next i body -1 del 2 endif !Flat Seat if swingType = 3 then add (a - seatLength)/2, -seatWidth/2, -linkThick - .020 prism 4, .020, -linkLength, 0, seatLength + linkLength, 0, seatLength + linkLength, seatWidth, -linkLength, seatWidth del 1 endif return "Quadratic Spine": nSpines = nSpines + 1 x1 = get(1): y1 = get(1): z1 = get(1) x2 = get(1): y2 = get(1): z2 = get(1) x3 = get(1): y3 = get(1): z3 = get(1) spineN = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t
446 Chapter 24: Example Objects - Playground Equipment/Swing spineN = spineN + 1 spineX[nSpines][spineN] = x1*s^2 + 2*x2*s*t + x3*t^2 spineY[nSpines][spineN] = y1*s^2 + 2*y2*s*t + y3*t^2 spineZ[nSpines][spineN] = z1*s^2 + 2*z2*s*t + z3*t^2 next t return "Cubic Spine": nSpines = nSpines + 1 x1 = get(1): y1 = get(1): z1 = get(1) x2 = get(1): y2 = get(1): z2 = get(1) x3 = get(1): y3 = get(1): z3 = get(1) x4 = get(1): y4 = get(1): z4 = get(1) spineN = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t spineN = spineN + 1 spineX[nSpines][spineN] = x1*s^3 + 3*x2*s^2*t + 3*x3*s*t^2 + x4*t^3 spineY[nSpines][spineN] = y1*s^3 + 3*y2*s^2*t + 3*y3*s*t^2 + y4*t^3 spineZ[nSpines][spineN] = z1*s^3 + 3*z2*s^2*t + 3*z3*s*t^2 + z4*t^3 next t return "Quartic Spine": nSpines = nSpines + 1 x1 = get(1): y1 = get(1): z1 = get(1) x2 = get(1): y2 = get(1): z2 = get(1) x3 = get(1): y3 = get(1): z3 = get(1) x4 = get(1): y4 = get(1): z4 = get(1) x5 = get(1): y5 = get(1): z5 = get(1) spineN = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t spineN = spineN + 1 spineX[nSpines][spineN] = x1*s^4 + 4*x2*s^3*t + 6*x3*s^2*t^2 + 4*x4*s*t^3 + x5*t^4 spineY[nSpines][spineN] = y1*s^4 + 4*y2*s^3*t + 6*y3*s^2*t^2 + 4*y4*s*t^3 + y5*t^4 spineZ[nSpines][spineN] = z1*s^4 + 4*z2*s^3*t + 6*z3*s^2*t^2 + 4*z4*s*t^3 + z5*t^4 next t
Chapter 24: Example Objects - Playground Equipment/Swing 447 return "Quadratic Rib": nRibs = nRibs + 1 x1 = get(1): y1 = get(1): z1 = get(1) x2 = get(1): y2 = get(1): z2 = get(1) x3 = get(1): y3 = get(1): z3 = get(1) xMid = (x1 + x3)/2 yMid = (y1 + y3)/2 zMid = (z1 + z3)/2 ux = x2 - xMid uy = y2 - yMid uz = z2 - zMid x2 = xMid + 2*ux y2 = yMid + 2*uy z2 = zMid + 2*uz ribN = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t ribN = ribN + 1 ribX[nRibs][ribN] = x1*s^2 + 2*x2*s*t + x3*t^2 ribY[nRibs][ribN] = y1*s^2 + 2*y2*s*t + y3*t^2 ribZ[nRibs][ribN] = z1*s^2 + 2*z2*s*t + z3*t^2 next t return "Cubic Rib": nRibs = nRibs + 1 ribN = 0 x1 = get(1): y1 = get(1): z1 = get(1) x2 = get(1): y2 = get(1): z2 = get(1) x3 = get(1): y3 = get(1): z3 = get(1) x4 = get(1): y4 = get(1): z4 = get(1) if i > 3 and i < nt - 1 then t0 = max(0, .45*sin((i - 2)*(180/(nt)))) else t0 = 0 endif
448 Chapter 24: Example Objects - Playground Equipment/Swing k = 0 for t = 0 to 1 step dt s = 1 - t k = k + 1 ribN = ribN + 1 ribX[nRibs][ribN] = x1*s^3 + 3*x2*s^2*t + 3*x3*s*t^2 + x4*t^3 ribY[nRibs][ribN] = y1*s^3 + 3*y2*s^2*t + 3*y3*s*t^2 + y4*t^3 ribZ[nRibs][ribN] = z1*s^3 + 3*z2*s^2*t + 3*z3*s*t^2 + z4*t^3 next t return
The 2D Script The 2D Script is given below. It closely follows the 3D Script but uses polygons rather than prisms. !2D Hotspots hotspot2 0, 0 hotspot2 a, 0 hotspot2 a/2, 0 !Lines for the Cables line2 0, 0, (a - seatLength)/2, 0 line2 a, 0, (a + seatLength)/2, 0 !Basic Swing Seat if swingType = 1 or swingType = 3 then hotspot2 (a - seatLength)/2, -seatWidth/2 hotspot2 (a + seatLength)/2, -seatWidth/2 hotspot2 (a - seatLength)/2, seatWidth/2 hotspot2 (a + seatLength)/2, seatWidth/2 poly2_b 4, 7 - 2*(gs_fill_pen = 0), gs_fill_pen, gs_back_pen, (a - seatLength)/2, -seatWidth/2, 1, (a + seatLength)/2, -seatWidth/2, 1, (a + seatLength)/2, seatWidth/2, 1, (a - seatLength)/2, seatWidth/2, 1 hotline2 0, 0, (a - seatLength)/2, 0 hotline2 a, 0, (a + seatLength)/2, 0 hotline2 (a - seatLength)/2, -seatWidth/2, (a + seatLength)/2, -seatWidth/2 hotline2 (a + seatLength)/2, -seatWidth/2,
Chapter 24: Example Objects - Playground Equipment/Swing 449 (a + seatLength)/2, seatWidth/2 hotline2 (a + seatLength)/2, seatWidth/2, (a - seatLength)/2, seatWidth/2 hotline2 (a - seatLength)/2, seatWidth/2, (a - seatLength)/2, -seatWidth/2 endif !Infant Swing Seat if swingType = 2 then hotspot2 (a - seatLength)/2, 0 hotspot2 (a + seatLength)/2, 0 add2 (a - seatLength)/2, .025 nt = 10 dt = 1/nt dim spineX[][], spineY[][], spineZ[][] put 0, 0, 0, 0, -.7*seatLength, 0, seatLength/2, -.7*seatLength, 0, seatLength, -.7*seatLength, 0, seatLength, 0, 0 gosub "Quartic Spine" for j = 1 to spineN put spineX[1][j], spineY[1][j], 1 next j poly2_b nsp/3, 7 - 2*(gs_fill_pen = 0), gs_fill_pen, gs_back_pen, get(nsp) del 1 endif end "Quartic Spine": nSpines = nSpines + 1 x1 = get(1): y1 = get(1): z1 = get(1) x2 = get(1): y2 = get(1): z2 = get(1) x3 = get(1): y3 = get(1): z3 = get(1) x4 = get(1): y4 = get(1): z4 = get(1) x5 = get(1): y5 = get(1): z5 = get(1)
450 Chapter 24: Example Objects - Wine Glass spineN = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t spineN = spineN + 1 spineX[nSpines][spineN] = x1*s^4 + 4*x2*s^3*t + 6*x3*s^2*t^2 + 4*x4*s*t^3 + x5*t^4 spineY[nSpines][spineN] = y1*s^4 + 4*y2*s^3*t + 6*y3*s^2*t^2 + 4*y4*s*t^3 + y5*t^4 spineZ[nSpines][spineN] = z1*s^4 + 4*z2*s^3*t + 6*z3*s^2*t^2 + 4*z4*s*t^3 + z5*t^4 next t return
24.3 Wine Glass Among other techniques, this object uses the revolve statement, cubic splines, group operations, images with transparency, linear algebra, the stack, and of course loops. Object Subtype The subtype selected for this object is Food Service Equipment. Parameters Parameters of the object include two materials (glassMat and wineMat), standard pens and fill (gs_cont_pen, gs_fill_type, gs_fill_pen, gs_back_pen), an integer for interface menu control (uiPage), an integer parameter for bitwise selection of the 3D model state (glassUsage), and length and angle parameters to control the shape of the wineglass (A, ZZYZX, bowlWidth, bowlHeight, bowlTopAngle, bowlBaseAngle, wineLevel, baseDiameter, baseThickCenter, stemThick, stemRadiusBase, stemRadiusTop). The Master Script The Master Script sets up some constants, calculates some derived values based on the parameter values, and limits the range of some of the input values. The purpose of the constants is to provide for future expansion. These values could have been included as parameters of the object, but this was considered unnecessary for the initial design. However, if the extra control is later found to be a requirement, it will be easy to add the parameters, remove the corresponding line of script from the Master Script, and all other scripts can remain unchanged. The derived values are included to avoid repeated calculation. For instance, the stem height is calculated based on the total height and the bowl height, then stored for re-use in the stemHeight variable. The final purpose of the Master Script is to provide a degree of error handling. In this case, we want to make some restrictions on the input dimensions in order to ensure a sensible looking wine glass.
Chapter 24: Example Objects - Wine Glass 451 !Total Height zzyzx = max(zzyzx, .025) !Bowl bowlTopAngle = -bowlTopAngle bowlThickLip = 0.0015 bowlThickBase = .003 bowlHeight = min(bowlHeight, zzyzx - baseThickCenter - 2*stemThick) bowlHeight = max(bowlHeight, .003) bowlWidth = max(bowlWidth, stemThick, .005) !Stem stemHeight = zzyzx - bowlHeight stemThick = max(stemThick, .0005) stemRadiusBase = min(stemRadiusBase, (baseDiameter - stemThick)/2 - .001, (stemHeight baseThickCenter)/2 - .001) stemRadiusBase = max(stemRadiusBase, 0) stemRadiusTop = min(stemRadiusTop, (bowlWidth - stemThick)/2 - .001, (stemHeight baseThickCenter)/2 - .001) stemRadiusTop = max(stemRadiusTop, 0) !Base baseThickEdge = 0.002 !Context Options invertGlass = bittest(glassUsage, 0) surfaceOnly = bittest(glassUsage, 1) hollowGlass = bittest(glassUsage, 2) includeWine = bittest(glassUsage, 3)
The Parameter Script The Parameter Script sets up values lists for the menu and the parts to model. !Set variables buttonID and modparName to stand in for globals buttonID = 0 modparName = "" isFirst = 0 rrr = Application_Query ("Parameter_Script", "FirstOccasion_in_Progress", isFirst) if isFirst then buttonID = glob_ui_button_ID modparName = glob_modpar_name endif
452 Chapter 24: Example Objects - Wine Glass !Menu values "uiPage" 1, 2, 3 !Parts to Model (defined bitwise) values "glassUsage" 0,
!Solid, upright, empty
4,
!Solid, inverted, empty
1,
!Hollow, upright, empty
1 + 4,
!Hollow, inverted, empty
1 + 4 + 8
!Hollow, upright, filled
!Force A to match the base diameter if modparName = "baseDiameter" then a = baseDiameter parameters a = a endif !Lock the wine level control unless the glass is filled if not(includeWine) then lock "wineLevel" endif
The 2D Script The 2D Script can be extremely simple, as the shape of a wine glass is always circular in plan view. pen gs_cont_pen fill gs_fill_type !Hotspot on the Glass Center hotspot2 0, 0 !Calculate the Bowl Diameter ux = cos(bowlBaseAngle) uy = sin(bowlBaseAngle) vx = cos(bowlTopAngle) vy = sin(bowlTopAngle) x1 = stemHeight + ux*stemRadiusTop y1 = stemThickTop/2 + uy*stemRadiusTop x3 = stemHeight + bowlHeight y3 = bowlWidth/2 !Calculate the intersection of a line through(x1, y1) having direction u !with a line through (x3, y3) having direction v D = vx*uy - ux*vy t = (ux*(y3 - y1) - uy*(x3 - x1))/D
Chapter 24: Example Objects - Wine Glass 453 x2 = x3 + t*vx y2 = y3 + t*vy dt = 1/9 RT = 0 for t = 0 to 1 + dt/2 step dt s = 1 - t RT = max(RT, y1*s^2 + 2*y2*s*t + y3*t^2) next t !Calculate the Base Diameter RB = baseDiameter/2 !Draw the 2D Symbol poly2_B 2, 7, gs_fill_pen, gs_back_pen, 0, 0, 901, max(RB, RT), 360, 4001 !Hot Arcs hotarc2 0, 0, max(RB, RT), 0, 180 hotarc2 0, 0, max(RB, RT), 180, 360
The 3D Script The 3D model a revolved form. The revolved section is drawn using splines through key points as defined by the parameter settings. pen gs_cont_pen resol 18 iHotspot = 0 !Glass material glassMat
The first key points are the base diameter and thickness. !Base Edge put 0, baseDiameter/2, 0, baseThickEdge, baseDiameter/2, 1
The next part of the shape is defined by the base thickness at the center and edge, and the ‘radius’ at the base of the stem. These are converted to spline control points, and hence to a set of vertices. !Stem Base Radius ux = baseThickEdge - baseThickCenter uy = baseDiameter/2 - stemThick/2
454 Chapter 24: Example Objects - Wine Glass L = sqr(ux^2 + uy^2) ux = ux/L uy = uy/L x1 = baseThickCenter + ux*stemRadiusBase y1 = stemThick/2 + uy*stemRadiusBase x2 = baseThickCenter y2 = stemThick/2 x3 = (stemHeight + baseThickCenter)/2 y3 = stemThick/2 dt = 1/7 for t = 0 to 1 + dt/2 step dt s = 1 - t put x1*s^2 + 2*x2*s*t + x3*t^2, y1*s^2 + 2*y2*s*t + y3*t^2, 1 next t
A similar trick is played to define the top of the stem. !Stem Top Radius x1 = stemHeight - stemRadiusTop y1 = stemThick/2 x2 = stemHeight y2 = stemThick/2 ux = cos(bowlBaseAngle) uy = sin(bowlBaseAngle) x3 = stemHeight + ux*stemRadiusTop y3 = stemThick/2 + uy*stemRadiusTop dt = 1/4 for t = 0 to 1 - dt/2 step dt s = 1 - t put x1*s^2 + 2*x2*s*t + x3*t^2, y1*s^2 + 2*y2*s*t + y3*t^2, 1 next t
The bowl outer surface is defined by setting up spline control points and calculating surface vertices. !Bowl dim bowlX[], bowlY[]
Chapter 24: Example Objects - Wine Glass 455 ux = cos(bowlBaseAngle) uy = sin(bowlBaseAngle) vx = cos(bowlTopAngle) vy = sin(bowlTopAngle) x1 = stemHeight + ux*stemRadiusTop y1 = stemThick/2 + uy*stemRadiusTop x3 = stemHeight + bowlHeight y3 = bowlWidth/2 !Calculate the intersection of !a line through(x1, y1) with direction u !a line through (x3, y3) with direction v D = vx*uy - ux*vy t = (ux*(y3 - y1) - uy*(x3 - x1))/D x2 = x3 + t*vx y2 = y3 + t*vy nt = 9 dt = 1/nt bowlN = nt + 1 iBowl = bowlN for t = 0 to 1 + dt/2 step dt s = 1 - t xi = x1*s^2 + 2*x2*s*t + x3*t^2 yi = y1*s^2 + 2*y2*s*t + y3*t^2 bowlX[iBowl] = xi bowlY[iBowl] = yi iBowl = iBowl - 1 put xi, yi, 1 - (t > 1 - dt/2) next t
The inner surface of the bowl is defined by offsetting the outer surface vertices by the bowl thickness. This approach is continued to where the bowl outer surface meets the stem upper radius. At that point, a spline is interpolated to finish curved base of the bowl inner surface. !Bowl Inner if hollowGlass then !Calculate the inner surface of the bowl - an offset by bowlThick !Calculate direction vector of each edge on the bowl dim bowlUx[],
456 Chapter 24: Example Objects - Wine Glass bowlUy[] for i = 1 to bowlN - 1 bowlUx[i] = bowlX[i + 1] - bowlX[i] bowlUy[i] = bowlY[i + 1] - bowlY[i] Li = sqr(bowlUx[i]^2 + bowlUy[i]^2) bowlUx[i] = bowlUx[i]/Li bowlUy[i] = bowlUy[i]/Li next i bowlUx[bowlN] = bowlUx[bowlN - 1] bowlUy[bowlN] = bowlUy[bowlN - 1] !Offset all edges for i = 1 to bowlN bowlThick = bowlThickLip + (bowlThickBase - bowlThickLip)*((i - 1)/bowlN) bowlX[i] = bowlX[i] - bowlThick*bowlUy[i] bowlY[i] = bowlY[i] + bowlThick*bowlUx[i] next i !Intersect the edges for i = 2 to bowlN - 1 D = bowlUx[i]*bowlUy[i - 1] - bowlUx[i - 1]*bowlUy[i] t = (bowlUx[i-1]*(bowlY[i]-bowlY[i-1])-bowlUy[i-1]*(bowlX[i]-bowlX[i-1]))/D xi = bowlX[i] + t*bowlUx[i] yi = bowlY[i] + t*bowlUy[i] bowlX[i] = xi bowlY[i] = yi next i !Construct the bowl inner surface wineLevelMin = 0 gosub "Bowl Inner Surface" endif
The glass is placed as either inverted or upright. if invertGlass then xform 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, stemHeight + bowlHeight else xform 0, 0, 1, 0,
Chapter 24: Example Objects - Wine Glass 457 0, 1, 0, 0, 1, 0, 0, 0 endif revolve nsp/3, 360, 3 - 2*surfaceOnly, get(nsp) del 1
The wine level is determined, and a cutting plane positioned at that height. Hotspots are provided to control this level graphically. !Wine if includeWine and not(invertGlass) then wineLevelMax = zzyzx wineLevel = max(0, wineLevel) wineLevel = min(zzyzx - wineLevelMin, wineLevel) !Hotspot to adjust the wine level addz wineLevelMin hotspot 0, 0, 0, iHotspot + 1, wineLevel, 1 + 128 hotspot 0, 0, wineLevel, iHotspot + 2, wineLevel, 2 hotspot 0, 0, -1, iHotspot + 3, wineLevel, 3 iHotspot = iHotspot + 3 del 1 material wineMat addz wineLevelMin + wineLevel cutplane 0 del 1
The wine is modeled using the same algorithm as the bowl inner surface. gosub "Bowl Inner Surface" xform 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0 revolve nsp/3, 360, 3, get(nsp) del 1 cutend endif end "Bowl Inner Surface":
458 Chapter 24: Example Objects - Wine Glass !Put the inner surface points onto the stack for i = 1 to bowlN - 1 put bowlX[i], bowlY[i], 1 next i !Complete the bowl base - form a symmetric spline !using the final bowlX, bowlY and it's tangent reflected through the x-axis x1 = bowlX[bowlN] y1 = bowlY[bowlN] x3 = bowlX[bowlN] y3 = -bowlY[bowlN] uxi = bowlX[bowlN] - bowlX[bowlN - 1] uyi = bowlY[bowlN] - bowlY[bowlN - 1] if abs(uyi) > .0001 then x2 = x1 - bowlY[bowlN]*uxi/uyi else x2 = x1 endif y2 = 0 dt = 1/8 for t = 0 to 0.5 - dt/2 step dt s = 1 - t xi = x1*s^2 + 2*x2*s*t + x3*t^2 yi = y1*s^2 + 2*y2*s*t + y3*t^2 wineLevelMin = xi put xi, yi, 1 - (t > 1 - dt/2) next t return
The Interface Script The header of the Interface Script sets the dialog name, then places a simple menu control for 3 pages – Model, Materials and Appearance (figure 364) ui_dialog "Wine Glass Settings" !Menu Control ui_infield{3} "uiPage", 0, 0, 120, 20, 2, "", 0, 0, 0, 0, 0, 0,
Figure 364 – In this case we’ll use a very simple control for the menu. Menu controls tend to be somewhat intricate, and there are a number of other techniques that are more useful to emphasize in this example.
Chapter 24: Example Objects - Wine Glass 459 "", "Model", 1, "", "Materials", 2, "", "Appearance", 3 !Initialize uiX, uiY uiX0 = 150: uiY0 = 25 uiX = uiX0: uiY = uiY0
The first page provides controls for the 3D model, including settings for the shape, and also for how the wine glass should appear (full, empty, inverted, solid or hollow) (figure 365). !Model if uiPage = 1 then ui_groupbox "Model", 125, 0, 444 - 125, 266 !Bowl Shape ui_outfield "Glass Shape", 135, uiY, 200, 14
Figure 365 – The shape and dimensions of the wine glass are addressed on the first page, including details such as the orientation and current state of the glass (i.e. full, empty, inverted, etc.).
uiY = uiY + 18 !Options ui_infield{3} glassUsage, uiX + 12, uiY, 40, 56, 2, "CT_S_WineGlassOptions_25x41", 5, 1, 25, 41, 25, 41, 1, "Solid", 0, 2, "Hollow", 4, 3, "Filled", 12, 4, "Inverted - Solid", 1, 5, "Inverted - Hollow", 5 uiY = uiY + 32 uiX = uiX + 75 !Bowl !Diameter at Rim ui_pict "CT_S_WineGlassRimDia_31x28", uiX + 15, uiY - 32, 31, 28, 1 ui_infield{2} bowlWidth, uiX, uiY, 60, 19 ui_tooltip "Bowl Diameter at the Rim" uiX = uiX + 75 !Bowl Height ui_pict "CT_S_WineGlassBowlDepth_31x28", uiX + 15, uiY - 32, 31, 28, 1
460 Chapter 24: Example Objects - Wine Glass ui_infield{2} bowlHeight, uiX, uiY, 60, 19 ui_tooltip "Bowl Depth" uiX = uiX + 75 !Wine Level ui_pict "CT_S_WineGlassWineLevel_31x28", uiX + 14, uiY - 32, 31, 28, 1 ui_infield{2} wineLevel, uiX, uiY, 60, 19 ui_tooltip "Fill Level" uiX = uiX + 75 uiX = uiX0 uiY = uiY + 30 uiY = uiY + 40 !Bowl Base Angle ui_pict "CT_S_WineGlassBaseTangent_31x28", uiX + 15, uiY - 32, 31, 28, 1 ui_infield{2} bowlBaseAngle, uiX, uiY, 60, 19 ui_tooltip "Tangent at Bowl Base" uiX = uiX + 75 !Bowl Top Angle ui_pict "CT_S_WineGlassTopTangent_31x28", uiX + 15, uiY - 32, 31, 28, 1 ui_infield{2} bowlTopAngle, uiX, uiY, 60, 19 ui_tooltip "Tangent at Bowl Top" uiX = uiX + 75 !Stem Radius (top) ui_pict "CT_S_WineGlassStemRadiusTop_31x28", uiX + 15, uiY - 32, 31, 28, 1 ui_infield{2} stemRadiusTop, uiX, uiY, 60, 19 ui_tooltip "Stem Radius" uiX = uiX + 75 !Height ui_pict "CT_S_WineGlassHeight_28x35", uiX + 19, uiY - 39, 28, 35, 1 ui_infield{2} zzyzx, uiX, uiY, 60, 19 ui_tooltip "Stem Radius" uiX = uiX + 75 uiX = uiX0 uiY = uiY + 40 !Base & Stem ui_outfield "Base & Stem", 135, uiY + 3, 200, 14 uiY = uiY + 18 uiY = uiY + 35 !Stem Thickness ui_pict "CT_S_WineGlassStemThick_31x26", uiX + 18, uiY - 30, 31, 26, 1 ui_infield{2} stemThick, uiX, uiY, 60, 19 ui_tooltip "Stem Diameter" uiX = uiX + 75 !Base Diameter
Chapter 24: Example Objects - Wine Glass 461 ui_pict "CT_S_WineGlassBaseDia_31x26", uiX + 18, uiY - 30, 31, 26, 1 ui_infield{2} baseDiameter, uiX, uiY, 60, 19 ui_tooltip "Base Diameter" uiX = uiX + 75 !Stem Radius ui_pict "CT_S_WineGlassStemRadiusBase_31x26", uiX + 18, uiY - 30, 31, 26, 1 ui_infield{2} stemRadiusBase, uiX, uiY, 60, 19 ui_tooltip "Stem Radius" uiX = uiX + 75 !Thickness at the Center ui_pict "CT_S_WineGlassBaseThick_31x26", uiX + 18, uiY - 30, 31, 26, 1 ui_infield{2} baseThickCenter, uiX, uiY, 60, 19 ui_tooltip "Base Thickness" uiX = uiX + 75 endif
The second page provides simple controls for the materials (figure 366). !Materials if uiPage = 2 then ui_groupbox "Materials", 125, 0, 444-125, 266 !Wine ui_infield{2} wineMat, uiX, uiY, 180, 23 uiX = uiX + 185 ui_outfield "Wine", uiX, uiY + 5, 440 - uiX, 14 uiX = uiX0 uiY = uiY + 26
Figure 366 – The second page lists all the materials used in the model. In this case only two materials are used.
!Glass ui_infield{2} glassMat, uiX, uiY, 180, 23 uiX = uiX + 185 ui_outfield "Glass", uiX, uiY + 5, 440 - uiX, 14 uiX = uiX0 uiY = uiY + 26 endif
The third page provides controls for the contour pen and background fill (figure 367). !Appearance if uiPage = 3 then
462 Chapter 24: Example Objects - Wine Glass ui_groupbox "Appearance", 125,0,444-125,266 !Contour Pens ui_outfield "Contour", 135, uiY, 200, 14 uiY = uiY + 18 !Pen ui_infield{2} gs_cont_pen, uiX, uiY, 45, 19 ui_outfield "Contour Pen" + " [" + str( gs_cont_pen, 1, 0) + "]", uiX + 50, uiY + 3, 90, 15 uiY = uiY + 22 uiY = uiY + 22 uiY = uiY + 15 !Background Fill (plan view) ui_outfield "Background Fill", 135, uiY, 200, 14
Figure 367 – The third page provides controls for pens and fills used by the wine glass. The glass uses the same contour pen for plan and 3D views. The plan symbol also uses a masking fill, so controls for this are provided. There is no reason to provide section fill and pens for this object, as it is intended for visualization rather than for working drawings.
uiY = uiY + 18 !Fill Type ui_infield{2} gs_fill_type, uiX, uiY, 180, 23 uiX = uiX + 185 ui_outfield "Masking Fill", uiX, uiY + 3, 440 - uiX, 14 uiY = uiY + 27 !Fill Pen uiX = 150 ui_infield{2} gs_fill_pen, uiX, uiY, 45, 19 ui_pict "cadi_FillHatchPen", uiX + 50, uiY + 2, 27, 14, 1 ui_outfield "[" + str(gs_fill_pen, 1, 0) + "]", uiX + 83, uiY + 2, 30, 14 uiY = uiY + 22 !Background Pen ui_infield{2} gs_back_pen, uiX, uiY, 45, 19 ui_pict "cadi_FillBkgdPen", uiX + 50, uiY + 2, 27, 14, 1 ui_outfield "[" + str(gs_back_pen, 1, 0) + "]", uiX + 83, uiY + 2, 30, 14 uiY = uiY + 22 endif
Chapter 24: Example Objects - Wine Glass 463 Images for the Settings Dialog A number of images are used in the Settings dialog. These images are used to convey the intention of the controls placed into the Settings dialog. The images used by the Wine Glass object are listed below: • A concatenated selection image.
• Images to indicate dimensions and angles.
o
Over-all height
o
Bowl shape
o
Base shape
o
Stem shape
o
Wine level
All the images include alpha channels to procude transparent backgrounds. Cheers!
Index
2 2D fill polygons background pen, 127 curved edges, 129 defining a polygon, 122 edge status, 127 extra control, 123 fill pen, 127 full circle, 132 holes, sub-polygons and end points, 131 mask to control contour and fill, 125 number of edges, 125 outline pen, 127 tangential arcs, 130 vertex co-ordinates, 127 2D full view, 25 2D script, 20, 107 2D fill polygons. See add2 command, 108 arc2 command, 117 circle2 command, 117 drawindex command, 133 drawing splines, 120 ellipses, 120 fill types. See attributes line types. See attributes line2 command, 116 mul2 command, 109 order in which the elements are drawn, 133 pens. See attributes rotation, 109 scaling the symbol, 109 transformations, 107 translation, 108 using drawing fragments, 112 2D symbol, 20, 107
projection, 115 using drawing fragments, 112
3 3D model, 2, 136 basic elements, 140 cuboid, 142 curved surfaces, 220 pens, fills and materials, 136 smoothness, 138 solid body, 138 surface model, 138 surfaces cut in section, 137 transformations, 139 wire frame, 138 3D script, 21, See 3D model 3D text, 181 3D view, 25
A add2 command, 108 adding vectors, 257 add-ons, 4 addx, addy, addz commands, 139 Adobe Photoshop, 184 algorithms run-time efficiency, 405 all keyword when calling a macro, 88 alpha channel, 184 and operator, 44 angles calculating an angle, 287 arc2 command, 117 ArchiCAD, 1 arcs and line types, 121
approximation, 220 drawing in 2D, 117 equation of a 2D arc through three points, 292 start and end angles, 117 zero and negative radii, 118 zero angles, 118 area of a polygon, 365 array variables, 32 copying data between arrays, 35 declaring arrays, 32 mixed text and number arrays, 33 scope, 36 variable names, 29 arrow heads, 117 attributes fill types, 110 line types, 110 pens, 110 auto-text, 50
B base command, 325 batch changes consistent parameters, 198 bit operators, 45 bitset operator, 46 bittest operator, 47 setting bit values, 46 testing bit values, 47 block element, 142 body command, 328 bounding box hotspots, 191 bprism_ element, 152 brackets, 41 bug-fixing macros & sub-routines, 86 buttons, 212, See custom settings palette function buttons, 212
picture buttons, 214
C C++, 4 calling macros, 87 cardboard cutout, 3 cardboard cutouts, 328 center of gravity of a polygon. See centroid centroid, 391 circle2 command, 117 circles drawing in 2D, 117 zero and negative radii, 118 zero angles, 118 circular fill polygon, 132 clamped cubic spline, 244 closed cubic spline, 243 closest point on a line, 298 on a line segment, 302 on a polygon, 390 commenting your code, 77 components vector components, 255 cone element, 143 constants, 26 converting points to millimeters, 170 counter variables, 29 cover fills, 125 cprism_ element, 151 create a new object, 8 create your own subtypes, 16 cross product, 261 cslab_ element, 153 cubic spline, 240 cuboid, 142 curve approximation arcs, 220 ellipses, 223
helices, 224 splines. See splines curved surfaces polygon count, 409 smoothness, 138 curves, 220 custom settings dialog buttons, 212, See buttons differences between Mac and Windows, 201 images& icons, 201 input fields, 203, See input fields labelling fields, 202 layout management, 205 organizing data, 215 pages and menus, 215 text style, 200 tool tips, 214 custom settings palette consistent parameter sets, 198 co-ordinates, 200 name, 199 palette size, 199 previewing the palette, 200 cut fills, 125 cylind element, 142 cylinders, 142
D date and time, 55 day, 55 details, 18 development cycle, 417 dim command, 32 dimension lines on icon images, 186 direction vectors, 254 display order, 135 distance between a point and a line, 305
between two lines, 306 distortion tubes, 158 doors & windows special settings, 19 dot product, 265 drafting fills, 125 drag-and-drop drawing elements, 115 drawindex command, 133 drawing fragments, 8 limitations, 114 use current attributes, 112 dynamic hotspots, 7 bounding box hotspots, 191 controlling other parameters indirectly, 196 display a different parameter, 197 edit two parameters simultaneously, 195 hiding reference hotspots, 195 rubber banding, 197 slow response, 197 to control a length in 2D, 192 to control a length in 3D, 193 to control an angle in 2D, 193 to control an angle in 3D, 194 dynamic hotspts. See hotspots
E edge command, 326 edge visibility 2D fill polygons, 128 editing library parts, 9 ellipse, 109 ellipses, 120 approximation, 223 ellipsoids, 143 else operator, 43 end command, 91 endwhile, 67 enhancements
macros & sub-routines, 86 Envirographic, 3 equals operator, 38 comparing real numbers, 39 equation of a line, 266 equation of a plane, 267 exclusive or operator. See exor operator exor operator, 45 extrude element, 153 extrusions, 147
F fields. See custom settings palette fill category, 125 fill polygons controlling the fill origin, direction and size, 133 fill types, 110 solid fill, 114 finding bugs by guesswork, 402 by printing values at runtime, 402 by simplifying the script, 403 in the parameter script, 402 infinite loops, 403 floor plan view. See 2D symbol font. See text formatting numbers decimal numbers, 51 special formatting, 52 for-next loop, 64 fragments, 111 freeform modeling, 324 cardboard cutouts, 328 constructing a 3D form, 340
G GDL, 1 GDL scripts, 19, See scripts
2D script, 20 3D script, 21 interface script, 23 master script, 19 parameter script, 22 property script, 23 get( ) function, 60 goal general goal, 415 project goal, 416 graphics. See images group operations, 161 defining a group, 163 placing a group, 164 slow 3D generation, 166 transformations, 163 groupbox, 215
H helices curve approximation, 224 holes in 2D fill polygons, 131 hotarcs, 190 hotlines, 190 hotspots, 189, See dynamic hotspots 2D symbol, 190 3D hotspots, 190 bounding box dimensions, 16 counter for dynamic hotspots, 29 hour, 55
I icons, 183, See images if operator, 42 IFC, 12 images, 183 3D icons, 186
for graphic selection infields, 187 for the settings dialog, 184 icon size, 184 with transparency, 3, 183, 450 inches, 33 infield command. See input fields infields graphic selection images, 187 infinite loops, 68 avoiding infinite loops, 71 end conditions, 70 finding the bug, 403 monkey typists, 73 practically infinite loops, 72 step size, 69 inherited parameters, 16 input fields angle parameters, 203 array parameters, 204 basic list, 206 Boolean parameters, 203 field size, 203 fill type parameters, 203 graphic selections, 206 integer parameters, 203 length parameters, 203 line type parameters, 203 listed inputs, 205 material parameters, 203 pen parameters, 204 interface script, 23 intersection of a line segment with an arc in 2D, 280 of two line segments in 2D, 279 of two lines in 2D, 276 of two lines in 3D, 289 of two polygons, 381
L length of a vector, 256 libraries, 1 folder structure, 189 library browser selecting an object, 189 library parts, 1 bounding box parameters, 15 changing the name, 189 create a new object, 8 details section, 18 editing an existing object, 9 editing environment, 6 editing window, 8 icons in the libary browser, 25 naming, 9 naming objects, 189 non-placeable, 10 parameters, 7 placeable, 10 preferences, 7 saving, 9 structure, 6 subtype, 11 lin_ command, 140 line segments 3D line segments, 140 drawing in 2D, 116 line types, 110 and arcs, 119, 121 and circles, 119 line2D command, 116 linear algebra, 253, 450, See vectors applications, 268 linear equations, 253 lines dashed lines in 3D, 141 points of closest approach, 310 logical operators, 42
and operator, 44 else operator, 43 exor operator, 45 if operator, 42 not operator, 44 or operator, 44 loops, 428, 431, See infinite loops and sub-routines, 70 for-next loop, 64 infinite loops, 68 practically infinite loops, 72 repeat-until loop, 67 slowdown due to complex processes, 75 step size, 68 while-endwhile loop, 67
M macros, 86 call command, 87 calling a macro, 87 creating a macro, 86 maintenance, 86 multi-tasking, 105 parameters keyword, 87 returned values, 89 returning complex data, 91 strengths and weaknesses, 103 tasks, 90 transferring data via the stack, 62 using the stack, 95 variable scope, 103 magnitude of a vector. See length of a vector maintenance, 427 macros and sub-routines, 86 master script, 19 common calculations, 83 matrices change of axes, 318
elementary transformation matrix, 316 matrix multiplication, 318 multiple transformations, 320 rotation about the origin, 316 scale, 318 matrix transformations, 316 max( ) function using the stack, 62 min( ) function using the stack, 62 minute, 55 model command, 138 model view options, 126 monkey typists, 73 month, 55 mul2 command, 109 mulx, muly, mulz commands, 139
N names changing the name of an object, 189 naming objects, 189 natural cubic spline, 240, 244 next, 64 normal vector, 263 not equals operator, 40 not operator, 44 numbers. See formatting numbers, See formatting numbers extracting numbers from a text string, 57
O object settings dialog. See custom settings palette 3D icons, 186 icon images, 184 images for graphic infields, 187 objects. See library parts operators, 38 and operator, 44
bit operators. See bit operators, See brackets, 41 else operator, 43 equals operator, 38 exor operator, 45 if operator, 42 logical operators, 42 not equals operator, 40 not operator, 44 or operator, 44 order of operations, 41 opportunity, 418 opposite vector, 264 optimization, 403 calculate only when required, 407 group operations, 413 low polygon count, 408 polygon operations, 394 provide optional primitive data, 413 testing the run-time speed, 404 use efficient algorithms, 405 optimizing for speed group operations, 166 or operator, 44 order of operations using brackets, 41 outfield command, 202, See custom settings dialog
P paragraphs defining a paragraph, 172 using paragraphs in text blocks, 176 parallel lines test for parallel lines, 273 parallel vector, 264 parameter script, 22 parameters, 7, 12 change a parameter, 14 control vs, simplicity, 13
default value, 13 inherited from the subtype, 16 interaction between parameters, 217 name, 13 parameter type, 7 type, 13 unique parameters, 15 variables, 13 working with batch selections, 218 parameters list, 13 bold, 14 change the order, 14 delete a parameter, 14 hide a parameter, 14 indent a parameter, 14 pens, 110 pgon command, 327 Photoshop, 184 pictures. See images pipg command, 332 placeable objects, 10 plane element, 147 planes 3D planes, 145 planning strategies, 422 points unit, 170 poly & poly_ elements, 145 poly2_ command, 123 poly2_B command, 123 poly2_B{2} command, 123 polygon count, 160 polygon operations, 364 adding two polygons, 372 area of a polygon, 365 centroid, 391 closest point on a polygon, 390 definition of a polygon, 364 does a point lie in a polygon?, 369
intersection of two polygons, 381 offset a polygon, 385 optimizing the operations, 394 polygon sense, 368 subtract one polygon from another, 383 polygons 2D fill polygons. See 2D fill polygons 3D polygons, 145 position vectors, 254 preferences, 7 preview picture, 25 selecting objects from the library, 189 preview window, 23 2D symbol, 24 drawing fragments, 23 primitive elements, 2 primitives, 324, 431 base command, 325 bodies, 328 edges, 326 picture polygons, 332 polygons, 327 vectors, 327 vertices, 325 prism, 428 prism element, 147 prism_ element, 149 prisms, 147 pro-active development, 419 progress, 417 projection, 115, 265 property script, 23 put( ) function, 60
Q quality control, 396 bug fixing, 402 identifying bugs. See finding bugs scripting style, 396
testing. See testing
R random numbers, 361 randomizing, 3 randomizing objects, 361 readability. See scripting style readable code, 76 repeat-until loop, 67 requests story_info, 34 resol command, 138, 409 resources improving the resource pool, 424 personal resources, 425 resources required for a project, 419 software tools, 426 return command, 97 returned_parameters keyword, 91 revolve element, 154 richtext2 command, 177 right hand rule, 262 rnd( ) function, 361 rot2 command, 109 rotation using matrices, 316 rotation in 2D, 109 rotations 3D rotations, 139 rotx, roty, rotz commands, 139 round 3D forms, 154
S saving objects, 9 scalar multiplication, 259 scalars, 254 scale in 2D, 109 scope. See sub-routines
script blocks, 82 scripting style, 76 commenting your code, 77 readability, 76 readability vs efficiency, 84 scoping variables, 83 script blocks, 82 structured scripting, 82 sub-routines, 83 variable names, 77 scripts, 8, See GDL scripts second, 55 sect_fill command, 137 section views cut surfaces, 137 selection fill polygons, 190 hotlines and hotarcs, 190 in the 3D view, 190 selection and referencing, 188 selecting an object from the library, 189 separator, 215 settings dialog, 7 shadows, 137 slab_ element, 152 thickness, 153 slabs, 147 solid element operations, 161 solid model, 138 speed. See optimization sphere element, 143 spheres, 143 smoothness, 144 spline2 command, 120 splines, 431 Bezier splines, 226 creating a 3D cubic spline, 244 cubic Bezier splines, 232 quadratic Bezier splines, 226 regular cubic splines, 240
split command, 57 spring, 224 stack, 431, 450 inserting data into a values list, 62 transferring data between macros, 62 transferring data to a sub-routine, 62 stack functions, 59 and sub-routines, 101 clearing the stack, 96 getting data, 60 putting data, 60 used with macros, 95 when to use the stack, 62 story_info request, 34 strings. See text strings, See text strings strlen( ) function, 49 strstr( ) function, 50 strsub( ) function, 50 structured scripting, 82 stw( ) function, 171 sub-routines, 86, 96 and loops, 70, 99 labeling sub-routines, 97 maintenance, 86 managing sub-routines, 103 return command, 97 re-usable sub-routines, 98, 101 running a sub-routine, 98 scope, 99 scoping variables, 83 strengths and weaknesses, 104 using the stack, 101 variations, 102 substituting text, 50 subtype importance to selection, 189 subtypes, 11 create a new subtype, 16 grouping objects by subtype, 17
selecting a subtype, 12 structured development, 17 using subtypes to transfer parameters, 18 surface model, 138
T template object, 428 testing, 427 automated testing, 401 developer testing. See testing exploratory testing, 400 on different operating systems, 401 testing by presentation, 401 through documenting, 401 teve command, 325 text, 167, See paragraphs 3D text, 181 defining a text style, 168 font and script type, 167 orientation, 178 placing a line of text in 2D, 171 text size, 170 width of a line of text, 171 text blocks, 176 placing a text block, 177 size of a text block, 177 text element, 181 text strings extracting numeric data, 57 including numbers. See formatting numbers length of a string, 50 number of characters, 49 reading part of a string, 50 strlen( ) function, 49 strstr( ) function, 50 strsub( ) function, 50 substituting one string with another, 50 text style
custom settings dialog, 200 texture mapping, 337 advanced control, 357 mapping a texture to a polygon, 358 mapping a texture to a solid body, 359 time current date and time, 55 tol command, 138 toler command, 410 tool tips, 214 transformations. See 2D script 3D transformations, 139 managing transformations, 84 translations, 321 3D translations, 139 transparent images, 183 Trees of the South Pacific, 3 tube, 428, 431 tube element, 157 continous loops, 159 smooth curves, 159 texture mapping, 160
U ui_groupbox command, 215 ui_separator command, 215 unique parameters, 15 unit normal vector, 263 unit vectors, 260 until, 67 user interface, 188, See custom settings palette selection and referencing. See selection and referencing
V values list, 431 values lists, 205 using the stack, 62 variables, 13, See parameters
array variable names, 29 array variables. See array variables assign a value, 26 assigning a result, 27 calculated variables, 26 changing the type of a variable, 32 counter variables, 29 declaring the type, 30 maximum name length, 29 naming variables, 28 numeric variables, 30 scope, 36 variable names, 77 variable type, 30 variables used in sub-routines, 99, 100 vector variables, 29, See array variables vect command, 327 vector addition using components, 258 vector variables. See array variables vectors, 254 addition, 257 cross product, 261 direction vectors, 254 dot product, 265 equivalent vectors, 257 length of a vector, 256 notation, 255 opposite vectors, 264 parallel vectors, 264 perpendicular in 2D, 264
position vectors, 254 scalar multiplication, 259 unit normal vector, 263 unit vectors, 260 variable names, 29 vector components, 255 vector diagrams, 256 zero vectors, 260 vert command, 325
W week, 55 while-endwhile loop, 67 white space. See scripting style wire frame model, 138
X xform command, 321 xor operator. See exor operator
Y year, 55
Z zero vectors, 260 ZZXZX, 15
Andrew Watson received his MSc in Physics from the University of Canterbury, New Zealand in 1993, and later a post-graduate diploma of education from Massey University. After teaching high school mathematics and physics he took a drafting position with a small architectural design company. There he started to explore GDL programming, and with encouragement from the local ArchiCAD reseller, Theometric Software was launched. The product range evolved to include Door & Window Builder, Stair Builder and Rapid Details (now Cadimage Doors+Windows, Cadimage Stairs and Cadimage Detail Elements). In 2004, Theometric Software was merged into Cadimage Ltd. Andrew continues to develop his GDL expertise and related skills while working on an expanded range of products and new projects. He now has over 10 years experience working as a full time programmer.