Mesh Operation - Example

__TOC__

Overview

This sample plugin will demonstrate how to create a very simple mesh operation that bevels some selected polygons. The evaluation function will handle the creation of the geometry, and the re-evaluation function will handle subsequent evaluations that simply offset the beveled geometry along its normal.

Bevel

The evaluation of the bevel is split into two separate operations, geometry creation and geometry offset. First, the new geometry is created in the same position as the original geometry, then the beveled polygons are offset along their normal. The point positions are cached after the geometry creation, so that in subsequent reevaluations we can easily apply a new offset to the original geometry position.

Visitor

The creation of the beveled geometry is handled by a visitor that is evaluated for each “selected” polygon. The visitor performs a simple bevel operation by duplicating the polygon points, updating the polygon to use the new points, and then creating “side” polygons to connect the new points to the old points. The new point positions are cached, so that they can easily be enumerated when performing a incremental update.

Class Variables

These three variables are initialised by the Mesh Operation when it instantiates the Visitor. The Polygon and Point objects are COM accessors that provide access to the elements, these types are automatically updated to point at the current element. The Vector is a cache of points that we create, so they can easily be enumerated by the incremental update.

1
2
3
CLxUser_Polygon          _polygon;
CLxUser_Point            _point;
std::vector <PointData> *_cache;

Evaluate

This function is called for every “selected” polygon. Loop over the points on the polygon. For each point, we store the Position and Normal, then duplicate the point by calling the Copy method. The old and new points are stored in temporary lists, so that we can easily enumerate over them to generate the side polygons. The normal, position and new point is also stored in the incremental data cache. The position is cached before an offset is applied, as we always want the offset to be calculated as an absolute distance from its original position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
_polygon.VertexCount (&pointCount);
for (unsigned int i = 0; i < pointCount; i++)
{
        PointData                pointData;
        LXtPointID               oldPoint = NULL;
        LXtFVector               position;

        if (LXx_OK (_polygon.VertexByIndex (i, &oldPoint)))
        {
                _point.Select (oldPoint);

                _point.Normal (polygon, pointData.normal);
                _point.Pos (position);
                LXx_VCPY (pointData.position, position);

                _point.Copy (&pointData.id);
                _cache->push_back (pointData);

                newPoints.push_back (pointData.id);
                oldPoints.push_back (oldPoint);
        }
}

Now the point has been copied and we’ve cached the point information, the polygon is updated to use the newly created points.

1
_polygon.SetVertexList (&newPoints[0], newPoints.size (), 0);

We should have an identical number of new points and old points, so we loop over the points and create polygons from the old ones to the new ones. Note the use of NewProto to create the polygon, this ensures that the new polygon inherits properties such as material tags from the original polygon.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if (oldPoints.size () == newPoints.size ())
{
        pointCount = oldPoints.size ();

        for (unsigned int i = 0; i < pointCount; i++)
        {
                LXtPointID               wallPoints[4] = {NULL};
                LXtPolygonID             wallPolygon = NULL;

                wallPoints[0] = oldPoints[i];
                wallPoints[1] = newPoints[i];
                wallPoints[2] = newPoints[(i+1)%pointCount];
                wallPoints[3] = oldPoints[(i+1)%pointCount];

                _polygon.NewProto (polygonType, wallPoints, 4, 1, &wallPolygon);
        }
}

Offset

The offset calculation is really simple, we loop over the cached points and for each one, we multiply the normal by the offset and then add that back to the cached position, this then becomes the new position for the point.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
for (i = 0; i < _points.size (); i++)
{
        PointData               *pointData = NULL;
        LXtVector                pos;

        pointData = &_points[i];

        if (LXx_OK (point.Select (pointData->id)))
        {
                LXx_VCPY (pos, pointData->normal);
                LXx_VSCL (pos, _offset);
                LXx_VADD (pos, pointData->position);

                point.SetPos (pos);
        }
}

Mesh Operation

The evaluation of this mesh operation is split into two sections, Evaluate and ReEvaluate. Evaluate will create some initial beveled geometry, and for subsequent compatible evaluations, ReEvaluate will be called to simply offset the original beveled geometry along it’s normal. When operating on many elements, this separate ReEvaluation pass can improve performance significantly.

Class Variables

The offset amount and cache of newly created points are stored as public class variables. When we convert an old compatible mesh operation to a new mesh operation, we must copy the variables from the old mesh operation to the new one.

1
2
double                   _offset;
std::vector <PointData>  _points;

Constructor

In the Mesh Operation constructor, a single attribute that is used to control the offset distance is defined using DynamicAttributes. A default value of 0.0 is set, this will be used as the default channel value when the mesh operation server is converted into an item.

1
2
dyna_Add ("offset", LXsTYPE_DISTANCE);
attr_SetFlt (0, 0.0);

Evaluate

Evaluation is relatively lightweight, as it simply wraps the visitor and offset functions.

Grab the offset attribute and cache it. This will be used to later on to test if the mesh operation is compatible. If the offset is 0.0, we early out without performing the bevel.

1
2
3
4
_offset = dyna_Float (0);

if (lx::Compare (_offset, 0.0) == LXi_EQUAL_TO)
        return LXe_OK;

Perform the bevel by instantiating the visitor, initialising its class variables, then calling the Enumerate function to touch every selected polygon. The mode argument that is passed to Enumerate function allows us to easily enumerate over the selected elements and skip unselected polygons. Once the polygons are beveled, OffsetPositions is called to perform an offset on the newly created points that were cached in the Visitor. Finally, SetMeshEdits is called to inform the system of the geometry change. The entire evaluation is wrapped in a batch, which can provide improved performance in some cases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (LXx_OK (mesh.BeginEditBatch ()))
{
        visitor._polygon.fromMesh (mesh);
        visitor._point.fromMesh (mesh);
        visitor._cache = &_points;

        visitor._polygon.Enumerate (mode, visitor, NULL);

        OffsetPositions (mesh);

        mesh.SetMeshEdits (LXf_MESHEDIT_GEOMETRY);
        mesh.EndEditBatch ();
}

Compare

In the compare function, the old mesh operation from a previous evaluation is passed in, and it should be tested to see if it’s cached state is compatible with the current properties of the mesh operation. A mesh operation is deemed compatible if the cached elements from the previous evaluation can simply be deformed to achieve an identical result to performing a full evaluation of the mesh operation.

lx::CastServer casts the old mesh operation COM interface into our internal implementation.

1
lx::CastServer (SERVER_NAME, other_obj, other);

If the current offset and previous offset are the same, we return LXiMESHOP_IDENTICAL to skip evaluation altogether. If either offset value was 0.0, then geometry in one of the evaluations will exist that won’t exist in the other, so we return LXiMESHOP_DIFFERENT to perform a full evaluation, otherwise we return LXiMESHOP_COMPATIBLE to perform an incremental update.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (other)
{
        if (lx::Compare (other->_offset, _offset) == LXi_EQUAL_TO)
                return LXiMESHOP_IDENTICAL;

        if (lx::Compare (other->_offset, 0.0) != LXi_EQUAL_TO && lx::Compare (_offset, 0.0) != LXi_EQUAL_TO)
                return LXiMESHOP_COMPATIBLE;
}

return LXiMESHOP_DIFFERENT;

Convert

If the previous evaluation was compatible, convert will be called to copy the point cache from the old mesh operation to the new one. We just copy values from one vector to another.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
lx::CastServer (SERVER_NAME, other_obj, other);

if (other)
{
        _points.clear ();

        for (unsigned int i = 0; i < other->_points.size (); i++)
        {
                _points.push_back (other->_points[i]);
        }
}

ReEvaluate

ReEvaluate is really simple, it just calls the OffsetPositions function which updates the position of the cached points.

1
2
3
4
if (LXx_OK (OffsetPositions (mesh)))
        return mesh.SetMeshEdits (LXf_MESHEDIT_POSITION);

return LXe_FAILED;

Server Tags

There are two server tags for this mesh operation, one specifies that the mesh operation should automatically be converted into an item, and the second specifies that the mesh operation supports polygon selections only.

1
2
3
4
5
6
LXtTagInfoDesc MeshOperation::descInfo[] =
{
        { LXsMESHOP_PMODEL,             "." },
        { LXsPMODEL_SELECTIONTYPES,     LXsSELOP_TYPE_POLYGON },
        { 0 }
};

User Interface

Like the majority of the MODO user interface, the UI for mesh operations is defined through XML config files. Configs are explained in some depth ./Category:Configs.

Command Help

The command help section is used to define the username of both the item and the items channels. Despite the server name being called “sample.bevel”, the item automatically generated from the mesh operation server has “.item” appended to the end, so all configs refer to the auto generated item as “sample.bevel.item”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<atom type="CommandHelp">
        <hash type="Item" key="sample.bevel.item@en_US">
                <atom type="UserName">Bevel Sample</atom>
                <hash type="Channel" key="enable">
                        <atom type="UserName">Enable</atom>
                </hash>
                <hash type="Channel" key="offset">
                        <atom type="UserName">Offset</atom>
                </hash>
        </hash>
</atom>

Properties Form

The properties form is defined using the “Attributes” block. The properties form contains a single control for displaying the offset channel, but also includes some common sheets to provide things like enable controls. The filter atom determines when the properties form should be displayed and is defined below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<atom type="Attributes">
        <hash type="Sheet" key="sample.bevel.item:sheet">
                <atom type="Label">Bevel Sample</atom>
                <atom type="Filter">sample.bevel.item:filterPreset</atom>
                <hash type="InCategory" key="itemprops:general#head">
                        <atom type="Ordinal">110</atom>
                </hash>
                <list type="Control" val="ref item-common:sheet">
                        <atom type="StartCollapsed">0</atom>
                        <atom type="Hash">#0</atom>
                </list>
                <list type="Control" val="ref meshoperation:sheet">
                        <atom type="StartCollapsed">0</atom>
                        <atom type="Hash">#1</atom>
                </list>
                <list type="Control" val="cmd item.channel sample.bevel.item$offset ?">
                        <atom type="Label">Offset</atom>
                        <atom type="StartCollapsed">0</atom>
                        <atom type="Hash">sample.bevel.item.offset.ctrl:control</atom>
                </list>
        </hash>
</atom>

The filter can be used to control when a form is displayed. They are defined by name, and have various nodes which dictate the rules for when a form should be displayed, in this case it uses the itemtype filter. The itemtype filter checks if the sample.bevel.item is selected.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<atom type="Filters">
        <hash type="Preset" key="sample.bevel.item:filterPreset">
                <atom type="Name">Bevel Sample</atom>
                <atom type="Description"></atom>
                <atom type="Category">pmodel:filterCat</atom>
                <atom type="Enable">1</atom>
                <list type="Node">1 .group 0 &quot;&quot;</list>
                <list type="Node">1 itemtype 0 1 &quot;sample.bevel.item&quot;</list>
                <list type="Node">-1 .endgroup </list>
        </hash>
</atom>

Categories

When the user adds the mesh operation, the mesh operation items are displayed in a browser that is sorted by categories. If no category is defined by the plugin, it will appear in a category called “Other”, however this can be overridden with the following config fragment. The “MeshOperations” category is the master category that shows all mesh operations, and “polygon” is a sub-category under “MeshOperations”. Various selection types are also provided for Vertex and Edge mesh operations, if the mesh operation supports multiple selection types, the category will need to be defined for each type.

1
2
3
4
5
6
7
8
9
<atom type="Categories">
        <hash type="Category" key="MeshOperations">
                <hash type="C" key="sample.bevel.item">polygon</hash>
        </hash>

        <hash type="Category" key="MeshOperationsPolygons">
                <hash type="C" key="sample.bevel.item">polygon</hash>
        </hash>
</atom>

The Code

Plugin

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#include <lxsdk/lxidef.h>
#include <lxsdk/lx_mesh.hpp>
#include <lxsdk/lx_pmodel.hpp>
#include <lxsdk/lxu_attributes.hpp>
#include <lxsdk/lxu_math.hpp>
#include <vector>

#define SERVER_NAME     "sample.bevel"

/*
 *      The PointData structure is used to cache the incremental data for each point.
 *      It stores the element pointer, the non-offset position, as well as the normal.
 */

struct PointData
{
        LXtPointID               id;
        LXtVector                position;
        LXtVector                normal;
};

/*
 *      The polygon visitor performs the bevel operation for a single polygon. It
 *      creates new points at the same position as the points on the polygon it is
 *      beveling. The polygon is then updated to use the new points and "wall" polygons
 *      are added bridging the old and new points. The new point information is cached
 *      to that it can work with the incremental re-evaluation.
 */

class PolygonVisitor : public CLxVisitor
{
        public:
                PolygonVisitor () : _cache (NULL) { }

                        void
                evaluate ()                     LXx_OVERRIDE
                {
                        std::vector <LXtPointID> newPoints;
                        std::vector <LXtPointID> oldPoints;
                        LXtPolygonID             polygon = NULL;
                        LXtID4                   polygonType = 0;
                        unsigned int             pointCount = 0;

                        polygon = _polygon.ID ();

                        /*
                         *      For each point on the polygon, create a new point that
                         *      matches it's position. Add the point information to
                         *      the cache.
                         */

                        _polygon.VertexCount (&pointCount);

                        for (unsigned int i = 0; i < pointCount; i++)
                        {
                                PointData                pointData;
                                LXtPointID               oldPoint = NULL;
                                LXtFVector               position;

                                if (LXx_OK (_polygon.VertexByIndex (i, &oldPoint)))
                                {
                                        _point.Select (oldPoint);

                                        /*
                                         *      Get the normal and position of the
                                         *      original point.
                                         */

                                        _point.Normal (polygon, pointData.normal);
                                        _point.Pos (position);
                                        LXx_VCPY (pointData.position, position);

                                        /*
                                         *      Copy the new point and store it in the
                                         *      cache.
                                         */

                                        _point.Copy (&pointData.id);
                                        _cache->push_back (pointData);

                                        /*
                                         *      Add the new and old points to a list
                                         *      so they can be enumerated to create
                                         *      the wall polygons.
                                         */

                                        newPoints.push_back (pointData.id);
                                        oldPoints.push_back (oldPoint);
                                }
                        }

                        /*
                         *      Update the polygon to use the new points.
                         */

                        _polygon.SetVertexList (&newPoints[0], newPoints.size (), 0);

                        /*
                         *      Create "wall" polygons linking the old and new points.
                         */

                        _polygon.Type (&polygonType);

                        if (oldPoints.size () == newPoints.size ())
                        {
                                pointCount = oldPoints.size ();

                                for (unsigned int i = 0; i < pointCount; i++)
                                {
                                        LXtPointID               wallPoints[4] = {NULL};
                                        LXtPolygonID             wallPolygon = NULL;

                                        wallPoints[0] = oldPoints[i];
                                        wallPoints[1] = newPoints[i];
                                        wallPoints[2] = newPoints[(i+1)%pointCount];
                                        wallPoints[3] = oldPoints[(i+1)%pointCount];

                                        _polygon.NewProto (polygonType, wallPoints, 4, 1, &wallPolygon);
                                }
                        }
                }

                CLxUser_Polygon          _polygon;
                CLxUser_Point            _point;
                std::vector <PointData> *_cache;
};

class MeshOperation : public CLxImpl_MeshOperation, public CLxDynamicAttributes
{
        public:
                MeshOperation ()
                {
                        /*
                         *      A single attribute is added to the mesh operation to
                         *      control the bevel offset. This attribute is converted
                         *      to a channel on the automatically generated item.
                         */

                        dyna_Add ("offset", LXsTYPE_DISTANCE);
                        attr_SetFlt (0, 0.0);
                }

                        LxResult
                mop_Evaluate (
                        ILxUnknownID             mesh_obj,
                        LXtID4                   type,
                        LXtMarkMode              mode)                  LXx_OVERRIDE
                {
                        CLxUser_Mesh             mesh (mesh_obj);
                        PolygonVisitor           visitor;
                        LxResult                 result = LXe_FAILED;

                        /*
                         *      If the offset amount is 0.0, then we want to do nothing.
                         */

                        _offset = dyna_Float (0);

                        if (lx::Compare (_offset, 0.0) == LXi_EQUAL_TO)
                                return LXe_OK;

                        if (LXx_OK (mesh.BeginEditBatch ()))
                        {
                                /*
                                 *      Enumerate over the polygons and bevel each one.
                                 */

                                visitor._polygon.fromMesh (mesh);
                                visitor._point.fromMesh (mesh);
                                visitor._cache = &_points;

                                visitor._polygon.Enumerate (mode, visitor, NULL);

                                /*
                                 *      Apply an offset to the beveled polygons.
                                 */

                                OffsetPositions (mesh);

                                mesh.SetMeshEdits (LXf_MESHEDIT_GEOMETRY);
                                mesh.EndEditBatch ();

                                result = LXe_OK;
                        }

                        return result;
                }

                        int
                mop_Compare (
                        ILxUnknownID             other_obj)             LXx_OVERRIDE
                {
                        MeshOperation           *other = NULL;

                        _offset = dyna_Float (0);

                        /*
                         *      Cast the other interface into our implementation, and
                         *      then compare the offset attribute.
                         */

                        lx::CastServer (SERVER_NAME, other_obj, other);

                        if (other)
                        {
                                /*
                                 *      If the offset is identical, we don't want to
                                 *      do anything.
                                 */

                                if (lx::Compare (other->_offset, _offset) == LXi_EQUAL_TO)
                                        return LXiMESHOP_IDENTICAL;

                                /*
                                 *      As long as neither offset is 0.0, the previous
                                 *      operation is compatible.
                                 */

                                if (lx::Compare (other->_offset, 0.0) != LXi_EQUAL_TO && lx::Compare (_offset, 0.0) != LXi_EQUAL_TO)
                                        return LXiMESHOP_COMPATIBLE;
                        }

                        return LXiMESHOP_DIFFERENT;
                }

                        LxResult
                mop_Convert (
                        ILxUnknownID             other_obj)             LXx_OVERRIDE
                {
                        MeshOperation           *other = NULL;

                        _offset = dyna_Float (0);

                        /*
                         *      Cast the other interface into our implementation, and
                         *      then copy the cached points that want to offset.
                         */

                        lx::CastServer (SERVER_NAME, other_obj, other);

                        if (other)
                        {
                                _points.clear ();

                                for (unsigned int i = 0; i < other->_points.size (); i++)
                                {
                                        _points.push_back (other->_points[i]);
                                }

                                return LXe_OK;
                        }

                        return LXe_FAILED;
                }

                        LxResult
                mop_ReEvaluate (
                        ILxUnknownID             mesh_obj,
                        LXtID4                   type)                  LXx_OVERRIDE
                {
                        CLxUser_Mesh             mesh (mesh_obj);

                        _offset = dyna_Float (0);

                        /*
                         *      Deform the cached points by reapplying the offset.
                         */

                        if (LXx_OK (OffsetPositions (mesh)))
                                return mesh.SetMeshEdits (LXf_MESHEDIT_POSITION);

                        return LXe_FAILED;
                }

                        LxResult
                OffsetPositions (
                        CLxUser_Mesh            &mesh)
                {
                        CLxUser_Point            point;
                        LxResult                 result = LXe_FAILED;
                        int                      i = 0;

                        if (mesh.test ())
                        {
                                point.fromMesh (mesh);

                                for (i = 0; i < _points.size (); i++)
                                {
                                        PointData               *pointData = NULL;
                                        LXtVector                pos;

                                        pointData = &_points[i];

                                        if (LXx_OK (point.Select (pointData->id)))
                                        {
                                                LXx_VCPY (pos, pointData->normal);
                                                LXx_VSCL (pos, _offset);
                                                LXx_VADD (pos, pointData->position);

                                                point.SetPos (pos);
                                        }
                                }

                                result = LXe_OK;
                        }

                        return result;
                }

                double                   _offset;
                std::vector <PointData>  _points;

                static LXtTagInfoDesc    descInfo[];
};

LXtTagInfoDesc MeshOperation::descInfo[] =
{
        { LXsMESHOP_PMODEL,             "." },
        { LXsPMODEL_SELECTIONTYPES,     LXsSELOP_TYPE_POLYGON },
        { 0 }
};

void initialize ()
{
        CLxGenericPolymorph     *srv = NULL;

        srv = new CLxPolymorph                                          <MeshOperation>;
        srv->AddInterface               (new CLxIfc_MeshOperation       <MeshOperation>);
        srv->AddInterface               (new CLxIfc_Attributes          <MeshOperation>);
        srv->AddInterface               (new CLxIfc_StaticDesc          <MeshOperation>);

        lx::AddServer (SERVER_NAME, srv);
}

Config

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

        <!--
                The CommandHelp block is used to define the item and channel usernames.
        -->

        <atom type="CommandHelp">
                <hash type="Item" key="sample.bevel.item@en_US">
                        <atom type="UserName">Bevel Sample</atom>
                        <hash type="Channel" key="enable">
                                <atom type="UserName">Enable</atom>
                        </hash>
                        <hash type="Channel" key="offset">
                                <atom type="UserName">Offset</atom>
                        </hash>
                </hash>
        </atom>

        <!--
                The properties form displays a single property to control the offset.
                The common mesh operation sheet is also included to add the enable
                checkbox to the top of the form.
        -->

        <atom type="Attributes">
                <hash type="Sheet" key="sample.bevel.item:sheet">
                        <atom type="Label">Bevel Sample</atom>
                        <atom type="Filter">sample.bevel.item:filterPreset</atom>
                        <hash type="InCategory" key="itemprops:general#head">
                                <atom type="Ordinal">110</atom>
                        </hash>
                        <list type="Control" val="ref item-common:sheet">
                                <atom type="StartCollapsed">0</atom>
                                <atom type="Hash">#0</atom>
                        </list>
                        <list type="Control" val="ref meshoperation:sheet">
                                <atom type="StartCollapsed">0</atom>
                                <atom type="Hash">#1</atom>
                        </list>
                        <list type="Control" val="cmd item.channel sample.bevel.item$offset ?">
                                <atom type="Label">Offset</atom>
                                <atom type="StartCollapsed">0</atom>
                                <atom type="Hash">sample.bevel.item.offset.ctrl:control</atom>
                        </list>
                </hash>
        </atom>

        <!--
                A filter determines when the properties form should be displayed. In this
                case the properties form should be displayed when the sample.bevel.item
                item type is selected.
        -->

        <atom type="Filters">
                <hash type="Preset" key="sample.bevel.item:filterPreset">
                        <atom type="Name">Bevel Sample</atom>
                        <atom type="Description"></atom>
                        <atom type="Category">pmodel:filterCat</atom>
                        <atom type="Enable">1</atom>
                        <list type="Node">1 .group 0 &quot;&quot;</list>
                        <list type="Node">1 itemtype 0 1 &quot;sample.bevel.item&quot;</list>
                        <list type="Node">-1 .endgroup </list>
                </hash>
        </atom>

        <!--
                The categories define which sub-category the mesh operation will appear
                in. Two categories have to be set, one that shows all Mesh Operations
                and another which is filtered to show only operations that support a
                specific selection type.
        -->

        <atom type="Categories">
                <hash type="Category" key="MeshOperations">
                        <hash type="C" key="sample.bevel.item">polygon</hash>
                </hash>

                <hash type="Category" key="MeshOperationsPolygons">
                        <hash type="C" key="sample.bevel.item">polygon</hash>
                </hash>
        </atom>

</configuration>