Direct Manipulation API, part 3: Manipulation feature
This guide will focus on the manipulation feature. Check this definition to learn what we mean by a feature.
The manipulation feature is similar to the creation feature
Just like a creation feature earlier in Part 1, 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 to create a Contextual Toolbar.
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 the pattern for actually manipulating things.
Manipulation
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 just a container for defining manipulation behavior. The usefulness of this class will become more apparent when we start using manipulators and handles.
Manipulators
Let us start with manipulators. Manipulators are essentially tools that aid the user in manipulating 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.
Handles
Next, let us look at the notion of handles. Handles are something users can act on to change the model. Handles are geometry objects like points or lines.
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 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 the 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 Direct Manipulation Platform inside Tekla Structures, so there is no need to manually initialize them. However, they should be disposed of, when no longer needed.
Implementing manipulation
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 sorts 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.
Easy addition of handles and manipulators
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.
Properties of ManipulationContext;
- 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.
Methods of ManipulationContext:
- AddManipulator(ManipulatorBase newManipulator). Method for adding manipulators into 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 Contextual Toolbar for an individual handle.
Contextual Toolbar
The last method mentioned above, SetHandleContextualToolbar, requires some explaining.
Firstly, this is the second way to define the Contextual Toolbar in a feature. The first one was defined earlier in Part 2: Contextual Toolbar.
Secondly, it is worth noting that once the Contextual Toolbar is loaded for the individual handle, it is not possible to get the one defined on the feature level back without re-initializing the feature. However, sometimes it is useful to have individual Contextual Toolbars 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().
Manipulation feature code example
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 boilerplate 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.
Code example 5
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);
}
}
}
Code 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.