Direct Manipulation API, part 1: Creation feature

Updated: 7 Feb 2024

What is a feature?

Feature is the most fundamental concept in the Direct Manipulation API. A feature is a class that is attached to a plugin by name and is then used by the Direct Manipulation Platform inside Tekla Structures to either create or manipulate the plugin. 

When a new feature is made, it is recommended to put it in a separate DLL file from the plugin file. This is not mandatory, but it does help keep the implementation cleaner.

Creation feature

The base class used for plugins for a creation feature is called Tekla.Structures.Plugins.DirectManipulation.Core.Features.PluginCreationFeatureBase.

To make a new feature, you just need to inherit from this class and implement three things:

  1. Default constructor: For a feature to be used, the Direct Manipulation Platform assumes there is a default constructor for it. This means the feature must have a constructor which does not take any parameters. The constructor for the base class has a string type parameter, and the argument for this parameter is used to bind the feature to the plugin by using the plugin name.
  2. The Initialize() method: When the feature becomes active, the Direct Manipulation API calls this method. It can be empty, meaning there is nothing to initialize, but if the feature does need some initial values to be used, this is the method to place such code into.
  3. The Refresh() method: When the feature is active and needs to refresh its state, this method gets called. This is useful when certain values change and the feature needs new or the most recent values to do something. This can become crucial when communicating with the plugin code.

These three aspects of the creation feature are shown in the code examples below.

Code example 1
namespace MyPluginNamespace
{
    // Any using directives.
    [Plugin(MyPluginName)]
    [PluginUserInterface("MyPlugin.MyUserInterface")]
    public sealed class MyPlugin : PluginBase
    {
        // Private fields.
        public const string MyPluginName = "MyPluginExample";
        // Constructors, properties and methods.
    }
}
Code example 1 continued
namespace MyPluginFeatures
{
    // Any other using directives.
    using MyPluginNamespace;
    using Tekla.Structures.Plugins.DirectManipulation.Core.Features;
 
    public sealed class MyPluginCreationFeature : PluginCreationFeatureBase
    {
        // Private fields.
        public MyPluginCreationFeature()
            : base(MyPlugin.MyPluginName)
        {
        }
        // Properties.
        protected override void Initialize()
        {
        }
        protected override void Refresh()
        {
        }
    }
}

Tools perform functions in the feature

It is fairly common to use different tools to perform different functions in the feature. Most tools can be found in the Tekla.Structures.Plugins.DirectManipulation.Services.Tools namespace.

PickingTool

One tool is called PickingTool, which can be found in the Picking sub-namespace. This tool, as the name suggests, is used to pick objects or points within the Tekla Structures model.

Creating an instance of the Picking Tool

A special aspect of this tool is that there is no public constructor for it. This is due to internal hooking-up of the underlying picker to work properly. To create an instance of a picking tool there is a static factory method called CreatePickingTool() in the Tekla.Structures.Plugins.DirectManipulation.Services.Utilities.ServiceFactory class that takes an InputRange object and InputTypes flag as arguments. These can be found in the Picking namespace.

The InputRange object is constructed using static factory methods from the same class, mainly

  • InputRange.AtMost(uint inputAmount)
  • InputRange.AtLeast(uint inputAmount) and 
  • InputRange.Exactly(uint inputAmount).

There is also a factory method called InputRange.InRangeOf(uint minAmount, uint maxAmount) to specify an exact input range for the plugin, for example from 2 to 5. Note, however, that if minAmount is greater than maxAmount, the method will throw an exception.

The InputRange object also has two public properties of type uint called Minimum and Maximum which can be used in code to validate the input amount.

Starting the picking tool

Once the picker has been constructed, the feature can start the picking session with a call to StartPickingSession(). This will highlight the mouse cursor in the model to indicate that the picking session is ongoing.

Depending on the input amount values given to the InputRange object, the picker will keep the session going until the minimum amount of input has been picked and end the session when the maximum amount has been reached.

The user can also request to end the session by clicking the middle mouse button. This is called input validation, and the feature can decide whether the picking session can be ended or if there is still input needed.

There are six separate events defined for the PickingTool. These are:

  1. ObjectPicked(object sender, ToleratedObjectEventArgs eventArgs). This event is invoked when an object in the model has been picked. The eventArgs argument contains four public properties:  These are, as the names suggest, the possible objects that can be picked.
    1. HitPoint 
    2. Objects
    3. Faces and
    4. Segments.
  2. InputValidationRequested(object sender, InputValidationEventArgs eventArgs). This event is invoked when the user presses the middle mouse button. The eventArgs argument has a single property called ContinueSession which can be set to true if the picking session should be continued. The default value is false.
  3. PickSessionEnded(object sender, EventArgs eventArgs). This event is invoked when the picking session has come to an end. Here any resources should be cleaned up and the input should be given to the plugin. It is also good to clean up any unnecessary preview graphics that have not been disposed of yet.
  4. PreviewRequested(object sender, ToleratedObjectEventArgs eventArgs). This event is used to draw any preview graphics needed for the placing of objects. A proper explanation of how the graphics work will be presented later.
  5. PickUndone(object sender, EventArgs eventArgs). This event is invoked when the user decides to undo the latest pick.
  6. PickSessionInterrupted(object sender, EventArgs eventArgs). This event is invoked when the user or the Direct Manipulation Platform inside Tekla Structures has interrupted the session. There is no way to continue the session once this happens, so any expensive resources needed during the session should be cleaned up.

Finishing the picking tool

When the PickingTool has finished picking the needed amount of input, an event handler for PickSessionEnded should be called. At this point, it might be necessary to check that the input is still valid.

In the upcoming example code, we assume for the sake of simplicity that the input is a set of points and that we only need to check that there are at least the minimum amount of points. To give the plugin the input, the feature must first initialize an input object of type Tekla.Structures.Model.ComponentInput and add the picked input to the input object by using one or more of its methods. The full documentation for this type can be found at API Reference. 

Once this has been done, the feature needs to only call CommitComponentInput() with the input object as the argument.

To exemplify this, we'll expand on the example previously shown.

Code example 2
namespace MyPluginFeatures
{
    // Any other using directives.
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using Tekla.Structures.Plugins.DirectManipulation.Core.Features;
    using Tekla.Structures.Plugins.DirectManipulation.Services.Tools.Picking;
    using Tekla.Structures.Plugins.DirectManipulation.Services.Utilities;
    using MyPluginNamespace;
 
    public sealed class MyPluginCreationFeature : PluginCreationFeatureBase
    {
        // Any other private fields.
        private readonly List<Point> pickedPoints = new List<Point>();
        private readonly InputRange inputRange;
        private PickingTool pickingTool;
 
        public MyPluginCreationFeature()
            : base(MyPlugin.MyPluginName)
        {
            this.inputRange = InputRange.AtMost(2);
        }
        // Properties
        protected override void Initialize()
        {
            this.DetachHandlers();
            this.pickingTool?.Dispose();
            this.pickingTool = this.CreatePickingTool(this.inputRange, InputTypes.Point);
            this.AttachHandlers();
            this.pickingTool.StartPickingSession("Pick two points.");
        }
        protected override void Refresh()
        {
            this.pickedPoints.Clear();
        }
        private void AttachHandlers()
        {
            if (this.pickingTool == null)
            {
                return;
            }
            this.pickingTool.ObjectPicked += this.OnObjectPicked;
            this.pickingTool.InputValidationRequested += this.OnInputValidationRequested;
            this.pickingTool.PickSessionEnded += this.OnPickEnded;
            this.pickingTool.PickUndone += this.OnPickingUndone;
        }
        private void DetachHandlers()
        {
            if (this.pickingTool == null)
            {
                return;
            }
            this.pickingTool.ObjectPicked -= this.OnObjectPicked;
            this.pickingTool.InputValidationRequested -= this.OnInputValidationRequested;
            this.pickingTool.PickSessionEnded -= this.OnPickEnded;
            this.pickingTool.PickUndone -= this.OnPickingUndone;
        }
        private void OnObjectPicked(object sender, ToleratedObjectEventArgs eventArgs)
        {
            if (!eventArgs.IsValid)
            {
                return;
            }
            this.pickedPoints.Add(eventArgs.HitPoint);
        }
        private void OnInputValidationRequested(object sender, InputValidationEventArgs eventArgs)
        {
            // This is for simply illustrative purposes. The proper way
            // to get this same functionality is to set the input range to
            // be exactly 2. The API takes care to keep the session going
            // until the minimum amount has been picked.
            // NOTE: When the session has been interrupted by the user, setting
            // the ContinueSession to true has no effect.
            if (this.pickedPoints.Count < Math.Max(this.inputRange.Minimum, 2))
            {
                eventArgs.ContinueSession = true;
            }
        }
        private void OnPickEnded(object sender, EventArgs eventArgs)
        {
            var input = new ComponentInput();
            input.AddInputPolygon(new Polygon { Points = new ArrayList(this.pickedPoints) });
            this.CommitComponentInput(input);
        }
        private void OnPickingUndone(object sender, EventArgs eventArgs)
        {
            if (this.pickedPoints.Count > 0)
            {
                this.pickedPoints.RemoveAt(this.pickedPoints.Count - 1);
            }
        }
    }
}

At this point, the feature is "on par" with the plugin's own input system. The plugin could be created without the creation feature using the Tekla Open API picker tool and normal input method.

Now it's time to take it up a level.

Showing a preview

Suppose we are making a plugin to place a beam component into the Tekla Structures model. It takes two input points and has a profile attribute. What is wanted is to have a preview of the beam before the second point is picked. To do this we utilize the previously introduced PreviewRequested event in the following way.

The Direct Manipulation API supports simple graphics using an interface called Tekla.Structures.Plugins.DirectManipulation.Core.IGraphicsDrawer. A property of this type called Graphics is already present in the base class, and one way to think of it is that it acts like a stylus.

These are the methods along with some overloads supported by the interface:

  1. Clear(). Clears all drawn graphics.
  2. DrawArc(Arc arc). Draws a given arc object. The optional parameter defines the line type for the drawing.
  3. DrawCircle(Point center, Vector normal, double radius). Draws a circle according to the given parameters.
  4. DrawCustomPart(string customPart, LineSegment direction, Vector offsetVector). Draws the graphics related to a named custom part. Optional parameters allow to define rotation for the part and linetype for the drawing.
  5. DrawExtrema(AABB extrema). Draws an extrema box based on an axis aligned boundary box. The axis is defined by the work plane coordinate system. The optional parameter defines the linetype of the drawing.
  6. DrawExtrema(LineSegment direction, double width, double height, Vector offsetVector). Similar to the previous method, but the extrema box can be defined in terms of height, width, direction, and offset. Optional parameters allow rotation along the direction axis and line type for the drawing.
  7. DrawFace(IEnumerable<Point> contourPoints). Draws a surface according to the contour defined by the contourPoints.
  8. DrawLine(Point startPoint, Point endPoint). Draws a single line between two points. The optional parameter defines linetype for the drawing.
  9. DrawLines(IEnumerable<LineSegment> polyline). Draws multiple lines. The optional parameter defines linetype for the drawing.
  10. DrawDimension(LineSegment line, Vector graphicNormal, DimensionEndPointSizeType sizeType). Draws a dimension graphic. Optional parameters define end point arrow orientation and size.
  11. DrawProfile(string profile, LineSegment direction, Vector offsetVector). Draws a named profile graphics. Similar to DrawCustomPart() above.
  12. DrawShape(string shape, LineSegment direction, Vector offsetVector). Draws a named shape object. Similar to DrawCustomPart() above.
  13. DrawText(string text, Point location). Draws a text object in the specified location. The optional parameter defines the text representation type.

For a beam plugin preview, let's say we know the profile name beforehand to be "HEA300". Now we can make use of the previously mentioned methods and properties, as shown in the code examples below.

Code example 3
namespace MyPluginNamespace
{
    // Any using directives.
    [Plugin(MyPluginName)]
    [PluginUserInterface("MyPlugin.MyUserInterface")]
    public sealed class MyPlugin : PluginBase
    {
        // Private fields.
        public const string MyPluginName = "MyPluginExample";
        public static readonly string DefaultProfileName = "HEA300";
        // Constructors, properties and methods.
    }
}
Code example 3 continued
namespace MyPluginFeatures
{
    // All prior using directives.
 
    public sealed class MyPluginCreationFeature : PluginCreationFeatureBase
    {
        // All the code mentioned prior.
        private void AttachHandlers()
        {
            // ...
            this.pickingTool.PreviewRequested += this.OnPreviewRequested;
        }
        private void DetachHandlers()
        {
            // ...
            this.pickingTool.PreviewRequested -= this.OnPreviewRequested;
        }
        private void OnPreviewRequested(object sender, ToleratedObjectEventArgs eventArgs)
        {
            this.Graphics.Clear();
            string profile = MyPlugin.DefaultProfileName;
            if (this.pickedPoints.Any())
            {
                this.Graphics.DrawProfile(
                    profile,
                    new LineSegment(
                        this.pickedPoints.Last(),
                        eventArgs.HitPoint),
                    new Vector(0, 0, -150),          // Offset to place the preview correctly.
                    90);                             // Rotation of 90 degrees to have the correct orientation in the preview.
            }
        }
    }
}

Customized user interface

The plugin can have a custom-made user interface that is attached to a data object for the plugin. This can affect the attributes of the plugin in a way that the creation feature is affected indirectly.

For example, if there is a field in our plugin UI that allows the user to change the profile for the beam, the preview of the creation feature should reflect this.

However, the connection between the plugin and the feature is just a binding by name. Fortunately, there is a way to pass information from the feature to the plugin and back in a relatively easy way, but this requires more explaining. More information on this later in Part 4: Communication.

 

What do you think about the Direct Manipulation API?

Post your feedback or questions on the Tekla Open API discussion forum.

 

Was this helpful?
The feedback you give here is not visible to other users. We use your comments to improve the content.