-
Notifications
You must be signed in to change notification settings - Fork 4
Documentation
The Direct Manipulation (DM) API for plugins is a subset of the full DM API made available to the public users. It targets the extension developers and provides support for plug-ins created by implementing the PluginBase class of OpenAPI. The exposed library mostly utilizes OpenAPI data types for geometry (e.g. Point, LineSegment instead of Vector3 and Segment3). The libraries aim to provide a set of streamlined and well-defined features, which are easier to use without knowing the implementation details of a DM feature. In other words, the extension developers can simply use the provided DM tool to achieve their goals without knowing the implementation details of the DM features.
The API is divided into three sub namespaces: Core, Services and Internal. Core can be thought of as the main body of the API that is subject to very little change over time. Any code written against classes in this namespace should be compatible between different versions of the API over time. The second namespace, Services, is used for other aspects of the API that the feature can use, like tools and handles. The third namespace, Internal, is reserved for changes in the internal implementation of the API and can be subject to radical change over time. Generally no feature aspect should be directly dependent on any class or method from this namespace!
Plugins are, in a nutshell, small programs, that are run from inside Tekla Structures. They are usually created as separate DLL files, that are placed in the environments/common/extensions/ directory, and launched using the Applications & components side pane in the main view of Tekla Structures. Most plugins in Tekla Structures are simple in the sense that they are simply inherited from the Tekla.Structures.Plugins.PluginBase class and override two key methods: DefineInput(), which returns a List<InputDefinition> type of object, and Run(List<InputDefinition> input) which returns a boolean. The first method defines the input of the plugin, and the second defines what the plugin does. The Run() method is important in the sense that it is not only called once but every time the plugin input is modified. There is a lot more to plugins than this, such as defining the Data object for communication purposes, using proper attributes like StructuresFieldAttribute, PluginAttribute and PluginUserInterfaceAttribute for important fields and classes, setting up a User Interface for the plugin using Tekla.Structures.Dialog.PluginFormBase or WPF, and utilizing the various methods provided by the OpenAPI for whatever the plugin requires. Some of these aspects come to play in the examples below as we start using the DM API to create and manipulate plugins in a model.
The most fundamental concept in the DM API is the concept of a feature. Basically a feature is a class which is attached to some plugin by name and is then used by the Direct Manipulation Platform (DM Platform) inside Tekla Structures to either create or manipulate the plugin. This section focuses on the creation feature.
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. The base class used for plugins for a creation feature is called Tekla.Structures.Plugins.DirectManipulation.Core.Features.PluginCreationFeatureBase. To make a new feature, the user just needs to inherit from this class and implement three things:
- Default constructor: For a feature to be used, the DM 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.
- The Initialize() method: When the feature becomes active, the DM 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.
- 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 example below:
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.
}
}
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()
{
}
}
}
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. One such tool is called PickingTool, that can be found in the Picking sub namespace. This tool as the name suggests is used to pick objects or points within the model. A special aspect about 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.
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 seven separate events defined for the PickingTool. These are:
- 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: HitPoint, Objects, Faces and Segments. These are, as the names suggest, the possible objects which can be picked.
- 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.
- 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.
- PreviewRequested(object sender, ToleratedObjectEventArgs eventArgs): This event is used to draw any preview graphics needed for the placing of objects. A proper explanation for how the graphics work will be presented later.
- PickUndone(object sender, EventArgs eventArgs): This event is invoked when the user decides to undo the latest pick.
- PickSessionInterrupted(object sender, EventArgs eventArgs): This event is invoked when the user or the DM platform 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.
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 https://developer.tekla.com/tekla-structures/api/7/13841. 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 the example previously shown:
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 OpenAPI picker tool and normal input method. Now it's time to take it up a level.
Suppose we are making a plugin to place a beam component into the 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 DM 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. There are ten methods along with some overloads supported by the interface listed below:
- Clear(): Clears all drawn graphics.
- DrawArc(Arc arc): Draws a given arc object. Optional parameter defines the line type for the drawing.
- DrawCircle(Point center, Vector normal, double radius): Draws a circle according to the given parameteres.
- 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.
- DrawExtrema(AABB extrema): Draws an extrema box based on an axis aligned boundary box. The axis is defined by the work plane coordinate system. Optional parameter defines the linetype of the drawing.
- 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 linetype for the drawing.
- DrawFace(IEnumerable<Point> contourPoints): Draws a surface according to the contour defined by the contourPoints.
- DrawLine(Point startPoint, Point endPoint): Draws a single line between two points. Optional parameter defines linetype for the drawing.
- DrawLines(IEnumerable<LineSegment> polyline): Draws multiple lines. Optional parameter defines linetype for the drawing.
- DrawDimension(LineSegment line, Vector graphicNormal, DimensionEndPointSizeType sizeType): Draws a dimension graphic. Optional parameters define end point arrow orientation and size.
- DrawProfile(string profile, LineSegment direction, Vector offsetVector): Draws a named profile graphics. Similar to DrawCustomPart() above.
- DrawShape(string shape, LineSegment direction, Vector offsetVector): Draws a named shape object. Similar to DrawCustomPart() above.
- DrawText(string text, Point location): Draws a text object in the specified location. Optional parameter defines the text representation type.
For a beam plugin preview, let's say we know the profile name before hand to be "HEA300". Now we can make use of the previously mentioned methods and properties:
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.
}
}
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.
}
}
}
}
It is possible for the plugin to have a custom-made user interface which 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 which allows the user to change 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 in the Communication section.
The contextual toolbar (CT) is a small helper toolbar that appears on screen every time an object in Tekla Structures has been selected. This section will focus on the various aspects of defining the controls for a feature in the contextual toolbar. However, since some controls rely on information flow from and to the plugin, we will leave that discussion for later in the Communication section.
There are actually two ways to create a CT. The first way will be discussed here, the second will be left for later. To create a CT for a feature the optional value useFeatureContextualToolBar in the constructor of the PluginCreationFeatureBase needs to be set to true and the method DefineFeatureContextualToolbar(IToolbar toolbar) should be overridden. The trick here is to use the parameter toolbar in the overriding. The interface Tekla.Structures.Plugins.DirectManipulation.Core.IToolbar defines a group of factory methods to create various controls for different uses. These are:
- ButtonControl CreateButton(): Creates a button control. The optional parameters allow the user to define a text and image element within the control.
- CheckBoxControl CreateCheckBox(): Creates a check box control. The optional parameters allow the user to define a text and image element associated with the control.
- DropDownListControl CreateDropDown(): Creates a drop-down list control. We will shortly go over some of the idiosyncrasies related to these controls with more information.
- LabelControl CreateLabel(): Creates a label control. The optional parameters allow the user to define a text and image element within the control.
- RadioButtonControl CreateRadioButton(): Creates a radio button control. The optional parameters allow the user to define a text and image element associated with the control along with a related control group.
- TextBoxControl CreateTextBox(): Creates a text box control. The optional parameters allow the user to define a text and image element within the control.
- ValueBoxControl CreateValueTextBox(): Creates a text box control for values of type double. The optional parameters allow the user to define the default starting value along with the representation type of the value. This can be either Distance or Angle.
- Remove(): Removes the control from the CT.
A control is always added to the toolbar, when calling one of these factory methods. Let us now go through some of these controls. A reader who is mostly familiar with these kinds of UI controls can mostly skip this part.
- ButtonControl: As the name suggests, the control is a basic button, that can have text and an image as internal elements. The most useful aspect of the button control is the Clicked event, which gets invoked, when the button is clicked.
- LabelControl: A simple label control, that can contain text or an image or both. There are no attached events to this control.
- CheckBoxControl: A small check box that can have its own label or image. This control supports a StateChanged event, that gets invoked any time the state of the box changes.
- DropDownListControl: This control is used to open a list of items to show various options to the user. There is an Add() method for adding new items as well as a StateChanges event that gets invoked any time the user changes the selection in the list.
- RadioButtonControl: This control is used in a situation, where there are multiple excluding options to choose from. The control is usually attached to a group of other same typed controls, where one of them is active and the rest are inactive. Access to the current group of the control can be gotten through the Group property of this control. To get or set the buttons checked state the IsChecked property can be used. A text and image element can be associated with the control. The RadioButtonControl also supports the StateChanged event.IsChecked
- TextBoxControl: A simple text box that can contain any text content. This control can be associated with a title text using the property Title. The title text will appear as a non-modifiable stylized text at the left side of the box. This can be used to give context to the value. The TextBoxControl supports StateChanged, GotKeyboardFocus and LostKeyboardFocus events.
- ValueBoxControl: A text box control, that is used to input and output values of type double. This control can also be associated with a title text using the property Title. The value of the control can be gotten and set through the Value property. The ValueBoxControl also supports StateChanged, GotKeyboardFocus and LostKeyboardFocus events.
All of these controls are inherited from a single base class called Tekla.Structures.Plugins.DirectManipulation.Core.ControlBase. The base class provides access to shared properties of the controls, such as Tooltip, Tag, Image and Text. The visibility of the control can also be set using the Visible property. It is also possible to add custom properties in the control as an object of type Dictionary<object, object> using CustomProperties property.
Now that we are familiar with all the different sorts of controls available to us, let us look at setting up the contextual toolbar in a feature. It is also possible to have backing fields for all controls in the feature class and simply turn off their visibility when they should not be shown. Continuing with the examples from the previous section, instead of starting the picking session at the initialization phase, let us have the session start when a button is clicked on the contextual toolbar:
namespace MyPluginFeatures
{
// All prior using directives.
using MyPluginNamespace;
public sealed class MyPluginCreationFeature : PluginCreationFeatureBase
{
// All prior private fields.
private ButtonControl button; // Backing field for the button.
// All prior Constructor(s) and Properties
protected override void DefineFeatureContextualToolbar(IToolbar toolbar)
{
this.button = toolbar.CreateButton("Start picking!");
button.Tooltip = "Helpful tooltips for everyone!";
button.Clicked += (sender, eventArgs) =>
{
this.pickingTool?.StartPickingSession("Pick two points.");
};
}
// Rest of the code.
}
}
This is now somewhat more useful, since the feature is now capable of starting multiple picking sessions instead of having to reinitialize the whole feature to start again.
This section will focus on the manipulation feature. Just like a creation feature, a manipulation feature must inherit from a respective base class. In this case that base class is Tekla.Structures.Plugins.DirectManipulation.Core.Features.PluginManipulationFeatureBase. Just like with the creation feature, the manipulation feature class is bound to the plugin using the plugin name, and the optional useFeatureContextualToolBar can be set to true.
Aside from some differences, the majority of the manipulation base is similar to the creation feature base class. The Initialize() and Refresh() methods behave as before, and the contextual toolbar creation pattern is the same as well. The biggest difference is really the pattern for actually manipulating things.
The PluginManipulationFeatureBase has a method in its base class called AttachManipulationContexts(). This method takes an argument of type Tekla.Structures.Model.Component, and returns a list of elements of type Tekla.Structures.Plugins.DirectManipulation.Core.ManipulationContext. A manipulation context is really just a container for defining manipulation behavior. The usefulness of this class will become more apparent when we start using manipulator and handles.
Let us start with manipulators. Manipulators are essentially tools that aid the user to manipulate the component in the model in some way. All manipulators can be found in the Tekla.Structures.Plugins.DirectManipulation.Services.Tools namespace. Different manipulators expose different events and properties depending on how they are meant to be used and what services they provide. All manipulators are disposable and should be properly cleaned up after use. Hooking up any manipulator requires care, and we will show how to do this once we start putting a manipulation behavior together.
Next let us look at the notion of handles. A handle in the API refers to a class that is inherited from Tekla.Structures.Plugins.DirectManipulation.Core.HandleBase. All handles can be found in the Tekla.Structures.Plugins.DirectManipulation.Services.Handles namespace. There are five essential handles:
- PointHandle: This is most likely the simplest kind of handle. The PointHandle handle represents a single draggable point in the model, and it exposes a property called Point of type Tekla.Structures.Geometry3d.Point.
- LineHandle: This handle represents a single draggable line in the model, and it exposes a property called Line of type Tekla.Structures.Geometry3d.LineSegment.
- PolycurveHandle: This handle is made up of multiple connected curves, and it allows the user to define the geometry of the total curve. However, due to the nature of this handle, it is advisable to use simpler handles when possible since they are more lightweight.
- ArcHandle: This handle is very similar to linehandle except that the underlying geometric object is of type Tekla.Structures.Geometry3d.Arc and is exposed through a property called Arc. The handle also exposes a property called Radius for defining the radius of the handle.
- PolygonalSurfaceHandle: This handle represents a draggable face of a component. The geometry of the surface can be defined as a contour using a collection of Point type objects, but the limitation is that these objects must belong to the same plane. The handle exposes the property called Contour of type IEnumerable<Point> to get and set the current contour of the face.
All handles support the DragStarted, DragOngoing and DragEnded events along with six base properties:
- IsDragOngoing: A boolean to express whether the handle is being dragged.
- IsHighlighted: A boolean to express whether the handle is being highlighted.
- IsSelected: A boolean to express whether the handle is currently selected.
- IsInvalid: A boolean to express whether the handle is valid. This may affect the appearance of the handle.
- IsVisible: A boolean to express whether the handle is visible.
- Tag: This property can be used to contain additional information about the handle.
A handle is always created using a factory method from Tekla.Structures.Plugins.DirectManipulation.Core.IHandleManager. In the base class of PluginManipulationFeatureBase there is a property called HandleManager, that is automatically instantiated and implements this interface. All created handles are automatically registered with the DM Platform, so there is no need to manually initialize them. However, they should be disposed of, when no longer needed.
Now that we know what handles and manipulators are we can look at how a manipulation behavior can be implemented. Generally speaking a manipulation behavior is simply the full pattern of how individual manipulators and handles interact. There are essentially two things to consider:
- Responsibility: How and who controls which parts of the manipulation feature. If two manipulators have access to the same information and both can manipulate it, do they notify each other about the change, or is there some other technique used here? These are issues to think about, when working with manipulators due to the nature of manipulators changing things in the model.
- Action: A manipulator needs to be hooked up correctly in order to work properly. What are the actions needed, and if multiple manipulators require the same actions, for example modifying the component, can these actions be isolated into simple methods, that the needed event handlers can just call?
Obviously these sort of things are not necessarily something that needs to be predefined, but there should be a guiding philosophy to follow, when making decisions about the design of the feature. For example, if we assume two manipulators can make committing changes to the plugin input by modifying the placing of the end points, then we can assume two things: 1) Both manipulators must share code to do the modification; 2) They both need access to the point handles in the model.
Also, just by looking at the feature class, it is not clear where the code to do all the hooking up should be placed. The Initialize() method is a good candidate, but there is an issue. The Initialize() method is run only at the initialization phase of the feature, which happens only, when the component gets selected, and even then only if the previously selected component wasn't running the same feature. In other words, if the user has created two beams with our plugin, and they have first selected one of them, and are now selecting the second one, the Initialize() method is not called. The user would have to deselect both components, and then select one of them to have the method called. However, the Refresh() method does get called when switching components.
What is needed here is something that contains the manipulation behavior and allows easy addition of handles and manipulators in general. For this purpose we have the ManipulationContext base class. As mentioned before, the AttachManipulationContexts() returns a list of manipulation contexts. This is so that an individual component can have multiple contexts depending if there are clear separate parallel manipulation behaviors at play. For example, if the component represents a staircase, the width of the component could be manipulated by one set of handles and manipulator and the height could be manipulated by another set. If these two sets are clearly defined not to interact together, they can and should be implemented as separate manipulation contexts. In this sense it is always up to the developer of the feature to define the proper contexts.
The ManipulationContext class defines four useful properties and four utility methods:
- Manipulators: List of currently added manipulators in the given instance of the context.
- Component: The instance of the component the context is attached to.
- Graphics: An instance of IGraphicsDrawer similar to the one in the feature base class.
- ParentFeature: The parent feature that owns the context.
- AddManipulator(ManipulatorBase newManipulator): Method for adding manipulators in to the context.
- UpdateContext(): Virtual method for the updating of the context. This method is used for defining the proper update order of handles and manipulators.
- ModifyComponentInput(ComponentInput input): Utility method for setting the new input for the component.
- SetHandleContextualToolbar(HandleBase handle, Action<IToolbar> defineToolbar): A method for defining a CT for an individual handle.
The last method mentioned above requires some explaining. Firstly, this is the second way to define the CT in a feature. Secondly, it is worth noting that once the CT is loaded for the individual handle, it is not possible to get the one defined on the feature level back without reinitializing the feature. However, sometimes it is useful to have individual CTs for handles. For example if a corner point of the component requires some special handling. Thirdly, the use of the method is also relatively simple in the sense that the method just needs the handle instance and a delegate similar to DefineFeatureContextualToolbar(). To bring all this together, let us look at an example.
One thing to consider in the next example is input modification. The original input of the plugin is present in the plugin data, but the format is not necessarily ideal. To deal with this issue, we must get and iterate through the input data, and introduce the new data in a parallel iteration. This is essentially boiler plate code, but depending on the input of the plugin, minor changes might need to be made. This example also generalizes the manipulation context for components made up of multiple beams.
namespace MyPluginFeatures
{
using System.Collections.Generic;
using Core;
using Core.Features;
using Tekla.Structures.Model;
using MyPluginNamespace;
public class MyPluginManipulationFeature : PluginManipulationFeatureBase
{
public MyPluginManipulationFeature()
: base(MyPlugin.PluginName, useFeatureContextualToolBar: true)
{
}
protected override void DefineFeatureContextualToolbar(IToolbar toolbar)
{
var button = toolbar.CreateButton("Hello World!");
button.Tooltip = "More helpful tooltips for all!";
}
protected override IEnumerable<ManipulationContext> AttachManipulationContexts(Component component)
{
yield return new MyPluginManipulationContext(component, this);
}
}
}
Example 5 continued
namespace MyPluginFeatures
{
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Core;
using Core.Features;
using Services.Handles;
using Services.Tools;
using Geometry3d;
using Model;
using HandleLocationType = Services.Handles.HandleLocationType;
public sealed class MyPluginManipulationContext : ManipulationContext
{
// Private fields
private readonly IHandleManager handleManager;
private readonly List<PointHandle> pointHandles;
private readonly List<LineHandle> lineHandles;
private readonly List<DistanceManipulator> manipulators;
// Constructor
public MyPluginManipulationContext(Component component, PluginManipulationFeatureBase feature)
: base(component, feature)
{
this.handleManager = this.ParentFeature.HandleManager;
this.pointHandles = this.CreatePointHandles(component);
this.lineHandles = this.CreateLineHandles(component);
this.manipulators = this.CreateManipulators(component, this.lineHandles);
this.AttachHandlers();
this.manipulators.ForEach(this.AddManipulator);
}
// Overrides
public override void UpdateContext()
{
this.UpdatePointHandles(this.Component, this.pointHandles);
this.UpdateLineHandles(this.Component, this.lineHandles);
this.UpdateDistanceManipulators();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this.DetachHandlers();
this.pointHandles.ForEach(handle => handle.Dispose());
this.lineHandles.ForEach(handle => handle.Dispose());
foreach (var manipulator in this.Manipulators)
{
manipulator.Dispose();
}
}
private void AttachHandlers()
{
this.pointHandles.ForEach(handle =>
{
handle.DragOngoing += this.OnPointHandleDragOngoing;
handle.DragEnded += this.OnPointHandleDragEnded;
});
this.lineHandles.ForEach(handle =>
{
handle.DragOngoing += this.OnLineHandleDragOngoing;
handle.DragEnded += this.OnLineHandleDragEnded;
});
this.manipulators.ForEach(manipulator =>
{
manipulator.MeasureChanged += this.OnMeasureChanged;
manipulator.MeasureChangeOngoing += this.OnMeasureChangeOngoing;
});
}
private void DetachHandlers()
{
this.pointHandles.ForEach(handle =>
{
handle.DragOngoing -= this.OnPointHandleDragOngoing;
handle.DragEnded -= this.OnPointHandleDragEnded;
});
this.lineHandles.ForEach(handle =>
{
handle.DragOngoing -= this.OnLineHandleDragOngoing;
handle.DragEnded -= this.OnLineHandleDragEnded;
});
this.manipulators.ForEach(manipulator =>
{
manipulator.MeasureChanged -= this.OnMeasureChanged;
manipulator.MeasureChangeOngoing -= this.OnMeasureChangeOngoing;
});
}
// Event handlers
private void OnPointHandleDragOngoing(object sender, DragEventArgs eventArgs)
{
this.DrawGraphics(this.pointHandles.Select(handle => handle.Point).ToList());
}
private void OnPointHandleDragEnded(object sender, DragEventArgs eventArgs)
{
this.ModifyInput(this.pointHandles.Select(handle => handle.Point).ToList());
}
private void OnLineHandleDragOngoing(object sender, DragEventArgs eventArgs)
{
this.DrawGraphics(this.GetLineHandlePoints(this.lineHandles, (LineHandle)sender));
}
private void OnLineHandleDragEnded(object sender, DragEventArgs eventArgs)
{
this.ModifyInput(this.GetLineHandlePoints(this.lineHandles, (LineHandle)sender));
}
private void OnMeasureChangeOngoing(object sender, EventArgs eventArgs)
{
this.DrawGraphics(this.pointHandles.Select(handle => handle.Point).ToList());
}
private void OnMeasureChanged(object sender, EventArgs eventArgs)
{
var distanceManipulator = sender as DistanceManipulator;
if (distanceManipulator == null)
{
return;
}
var currentManipulatorIndex = this.manipulators.IndexOf(distanceManipulator);
var points = this.manipulators.Select(m => m.Segment.StartPoint).ToList();
if (currentManipulatorIndex != this.manipulators.Count - 1)
{
points[currentManipulatorIndex + 1] = distanceManipulator.Segment.EndPoint;
}
points.Add(this.manipulators.Last().Segment.EndPoint);
this.ModifyInput(points);
}
// Graphics
private void DrawGraphics(List<Point> points)
{
this.Graphics.Clear();
var profile = "HEA200";
for (var i = 1; i < points.Count; i++)
{
var lineSegment = new LineSegment(points[i - 1], points[i]);
this.Graphics.DrawProfile(profile, lineSegment, new Vector(0, 0, -100), 90);
}
}
// Manipulators
private List<DistanceManipulator> CreateManipulators(
Component component,
IReadOnlyList<LineHandle> handles)
{
var manipulatorList = new List<DistanceManipulator>();
var distanceManipulators = handles.Select(
handle => new DistanceManipulator(component, this, handle.Line))
.ToList();
manipulatorList.AddRange(distanceManipulators);
return manipulatorList;
}
private void UpdateDistanceManipulators()
{
for (var i = 0; i < this.lineHandles.Count; i++)
{
this.manipulators[i].Segment = this.lineHandles[i].Line;
}
}
private ArrayList GetCurrentInput(Component component)
{
var inputArrayList = new ArrayList();
var originalInput = component.GetComponentInput();
if (originalInput == null)
{
return inputArrayList;
}
foreach (var inputItem in originalInput)
{
var item = inputItem as InputItem;
if (item == null)
{
continue;
}
switch (item.GetInputType())
{
case InputItem.InputTypeEnum.INPUT_1_POINT:
inputArrayList.Add(item.GetData() as Point);
break;
case InputItem.InputTypeEnum.INPUT_2_POINTS:
inputArrayList.AddRange(item.GetData() as ArrayList ?? new ArrayList());
break;
case InputItem.InputTypeEnum.INPUT_POLYGON:
inputArrayList.AddRange(item.GetData() as ArrayList ?? new ArrayList());
break;
default:
break;
}
}
return inputArrayList;
}
// Handles
private List<PointHandle> CreatePointHandles(Component component)
{
var handles = new List<PointHandle>();
var inputArrayList = this.GetCurrentInput(component);
foreach (Point point in inputArrayList)
{
var handle = this.handleManager.CreatePointHandle(point, HandleLocationType.InputPoint, HandleEffectType.Geometry);
this.SetHandleContextualToolbar(handle, t => this.DefinePointHandleContextualToolbar(handle, t));
handles.Add(handle);
}
return handles;
}
private void DefinePointHandleContextualToolbar(PointHandle handle, IToolbar toolbar)
{
var index = this.pointHandles.IndexOf(handle);
toolbar.CreateButton("Success! " + index);
}
private void UpdatePointHandles(Component component, List<PointHandle> handles)
{
var inputArrayList = this.GetCurrentInput(component);
var index = 0;
foreach (Point input in inputArrayList)
{
handles[index].Point = input;
index++;
}
}
private List<LineHandle> CreateLineHandles(Component component)
{
var handles = new List<LineHandle>();
var inputArrayList = this.GetCurrentInput(component);
for (var i = 1; i < inputArrayList.Count; i++)
{
var lineSegment = new LineSegment((Point)inputArrayList[i - 1], (Point)inputArrayList[i]);
var handle = this.handleManager.CreateLineHandle(lineSegment, HandleLocationType.MidPoint, HandleEffectType.Geometry);
handles.Add(handle);
}
return handles;
}
private void UpdateLineHandles(Component component, List<LineHandle> handles)
{
var inputArrayList = this.GetCurrentInput(component);
var index = 0;
for (var i = 1; i < inputArrayList.Count; i++)
{
var lineSegment = new LineSegment((Point)inputArrayList[i - 1], (Point)inputArrayList[i]);
handles[index].Line = lineSegment;
index++;
}
}
private List<Point> GetLineHandlePoints(List<LineHandle> handles, LineHandle handle)
{
int currentHandleIndex = handles.IndexOf(handle);
var points = this.lineHandles.Select(h => h.Line.StartPoint).ToList();
if (currentHandleIndex != this.manipulators.Count - 1)
{
points[currentHandleIndex + 1] = handle.Line.EndPoint;
}
points.Add(handles.Last().Line.EndPoint);
return points;
}
private void ModifyInput(List<Point> points)
{
this.Graphics.Clear();
var originalInput = this.Component.GetComponentInput();
if (originalInput == null)
{
return;
}
var input = new ComponentInput();
var index = 0;
foreach (var inputItem in originalInput)
{
if (!(inputItem is InputItem item))
{
continue;
}
switch (item.GetInputType())
{
case InputItem.InputTypeEnum.INPUT_1_OBJECT:
input.AddInputObject(item.GetData() as ModelObject);
break;
case InputItem.InputTypeEnum.INPUT_N_OBJECTS:
input.AddInputObjects(item.GetData() as ArrayList);
break;
case InputItem.InputTypeEnum.INPUT_1_POINT:
input.AddOneInputPosition(points[index]);
index++;
break;
case InputItem.InputTypeEnum.INPUT_2_POINTS:
input.AddTwoInputPositions(points[index], points[index + 1]);
index += 2;
break;
case InputItem.InputTypeEnum.INPUT_POLYGON:
var polygon = new Polygon();
foreach (var point in points)
{
polygon.Points.Add(new Point(point));
}
input.AddInputPolygon(polygon);
break;
default:
break;
}
}
this.ModifyComponentInput(input);
}
}
}
Generally a manipulation context can be relatively large, so for more simplicity and tidiness the code could be arranged into small utility classes or set up in partial classes that separate the boilerplate code from other special handling code.
The creation and manipulation feature must at times exchange information with the plugin. The input system introduced so far can only give and modify input data for the plugin, but to exchange information with a feature requires more effort. To start things off, we must first discuss how plugins store their respective data.
When a plugin is made for Tekla Structures, there is usually some kind of internal information, that is not necessarily exposed through the instance to the system but rather through an external data structure. In our example we could simply call it PluginData. The important thing about this data structure is that it is mainly made up of data fields that use the Tekla.Structures.Plugins.StructuresFieldAttribute attribute to decorate the data to name the fields. To give an example, let us continue with the example given in earlier sections to present this idea:
namespace MyPluginNamespace
{
public class PluginData
{
[StructuresField(PluginPropertyNames.Name)]
public string partName;
[StructuresField(PluginPropertyNames.Profile)]
public string profile;
[StructuresField(PluginPropertyNames.Offset)]
public double offset;
[StructuresField(PluginPropertyNames.Material)]
public string material;
}
public struct PluginPropertyNames
{
public const string Name = "name";
public const string Profile = "profile";
public const string Offset = "offset";
public const string Material = "material";
}
}
This data structure now defines four different named fields for the plugin. On the plugin side the data could be consumed like so:
namespace MyPluginNamespace
{
// Any using directives.
[Plugin(MyPluginName)]
[PluginUserInterface("MyPlugin.MyUserInterface")]
public class MyPlugin : PluginBase
{
// Any other private fields.
private string partName = string.Empty;
private string profile = string.Empty;
private string material = string.Empty;
private double offset = 0.0;
public const string MyPluginName = "MyPluginExample";
// Initialize the Data property in the constructor.
public MyPlugin(PluginData data)
{
this.Data = data;
}
// The actual data object.
private PluginData Data { get; set; }
public override bool Run(List<InputDefinition> Input)
{
try
{
this.GetValuesFromDialog();
// Use the data to make the plugin component.
}
catch (Exception Exc)
{
MessageBox.Show(Exc.ToString());
}
return true;
}
public override List<InputDefinition> DefineInput()
{
// Define input here.
}
// Get the values from the data object here.
private void GetValuesFromDialog()
{
this.partName = Data.partName;
this.profile = Data.profile;
this.material = Data.material;
this.offset = Data.offset;
if (IsDefaultValue(this.partName))
this.partName = "TEST";
if (IsDefaultValue(this.profile))
this.profile = "HEA200";
if (IsDefaultValue(this.material))
this.material = "STEEL_UNDEFINED";
if (IsDefaultValue(this.offset))
this.offset = 0;
}
}
}
The important thing to note here is that the plugin is using the data object to set all the necessary private fields to run the Run() method properly. Therefore, it should only be necessary to obtain a reference to this data object and run the plugin code again to make the necessary changes. This is, in a nutshell, the crux of the communication flow between the features and the plugin. Let us look at some examples of use to become more familiar with the patterns of communication in the features. In the Contextual toolbar section we introduced the idea of creating controls for the feature. Let us now set up a way for the feature to communicate with the plugin using the contextual toolbar:
namespace MyPluginFeatures
{
// Any other using directives.
using MyPluginNamespace;
public sealed class MyPluginManipulationFeature : PluginManipulationFeatureBase
{
// The component offset.
private double offset = 0.0;
/// Value box that contains the value of the offset.
private ValueBoxControl offsetValueBox;
// Rest of the class as before.
protected override void DefineFeatureContextualToolbar(IToolbar toolbar)
{
this.offsetValueBox = toolbar.CreateValueTextBox();
this.offsetValueBox.Tooltip = "Top radius";
this.offsetValueBox.Title = "offset=";
this.offsetValueBox.StateChanged += (control, eventArgs) =>
{
this.ModifyAttribute(PropertyNames.Offset, this.offsetValueBox.Value);
};
}
protected override void Refresh()
{
this.GetCurrentValues(this.Components.First());
// Rest of the method.
}
// Rest of the class as before
private void GetCurrentValues(Component component)
{
if (!(component.Select() && component.GetAttribute(PluginPropertyNames.Offset, ref this.offset)))
{
this.offset = 0.0;
}
this.offsetValueBox.Value = this.offset;
}
}
}
Using this same pattern, it is possible to communicate with the plugin data also from the creation feature. For example, at the end of a picking session, the PickSessionEnded event is invoked. In the CT control, we can set up a default value which can be modified by the user and which gets set in the data object before committing the input data:
namespace MyPluginFeatures
{
// Any other using directives.
using System.Collections;
using System.Linq;
using MyPluginNamespace;
public sealed class MyPluginCreationFeature : PluginCreationFeatureBase
{
// Any other private fields.
private readonly InputRange inputRange;
private ValueBoxControl offsetValueBox;
private PickingTool pickingTool;
public MyPluginCreationFeature()
: base(MyPlugin.MyPluginName)
{
this.inputRange = InputRange.AtMost(2);
}
// Constructor and Properties
protected override void DefineFeatureContextualToolbar(IToolbar toolbar)
{
this.offsetValueBox = toolbar.CreateValueTextBox(0.0);
}
private void OnPickEnded(PickingTool sender)
{
var input = new ComponentInput();
input.AddInputPolygon(new Polygon { Points = new ArrayList(this.pickedPoints) });
this.Component.SetAttribute(PropertyNames.Offset, this.offsetValueBox.Value);
this.CommitComponentInput(input);
}
}
}
With all this, we have a way to convey information from the creation feature to the manipulation feature using the plugin data object. These methods may seem simple but they are powerful tools for creating easy-to-use plugins with DM creation and manipulation features.