Hex Nut Part 1: Basics

Nuttin' Honey

Create your first useful Onshape feature: a hex nut. Adam walks you step-by-step through building 58 lines of simple, human-readable code, explaining helpful tips and tricks along the way.

This will be the first in a series of videos building on the hex nut example, covering all of the basic concepts needed for creating full-fledged Onshape features.


Query Filters

The Query interface is quite possibly the single most important concept in working with FeatureScript. Queries allow our code to access geometry in the Part Studio.

A query parameter like this asks the user for a piece of geometry. By adding a "Filter" tag, we tell Onshape that it should only accept geometry with certain characteristics, in this case sketch vertices. 

The && operator is a common programming convention meaning "AND". In this case, it means that our filter requires that any user selection be both of EntityType.VERTEX and that it have the property SketchObject.YES. Any selection that doesn't meet both of these criteria will be rejected.

annotation { "Name" : "Point", "Filter" : EntityType.VERTEX && SketchObject.YES, "MaxNumberOfPicks" : 1 }
        definition.point is Query;
 

Evaluate Functions

Queries are just little street signs pointing to certain geometry, but are not geometry in themselves. When we query a sketch vertex, for example, the variable "definition.point" does not refer to any actual piece of geometry, but rather to a street sign that points to a piece of geometry. In order to learn about the geometry itself, we need to evaluate the query.

In this case, we'll use the evVertexPoint() function, which accepts our query as input and returns a vector with the (X, Y, Z) coordinates of our vertex.

var origin = evVertexPoint(context, {
                "vertex" : definition.point
        });

debug(context, origin); // prints a vector() with the xyz coords of the selected vertex
 

Maps

Finding our sketch normal is slightly more complex. Retrieving the sketch normal actually happens in two steps: 1) get the sketch plane for our queried vertex, and 2) find the normal of that sketch plane. In order for this to make sense, we first need to understand maps in FeatureScript.

// A 'map' in FeatureScript is just a bunch of key:value pairs.
var fruitColorsMap = {'apple': 'red', 'banana': 'yellow'};

// We can access members of a map using dot syntax: mapName.key
debug(context, fruitColorsMap.apple); // prints "red"

This concept is very common in object-oriented programming (OOP) languages, and we'll use it heavily in FeatureScript. In fact, every single FeatureScript you write makes use of a map: the definition variable. Remember how we define our parameters using definition.whatever? That's because definition is a map variable, and it contains a bunch of key:value pairs, one for each parameter in our function.

A Plane object in FeatureScript is just a map containing three key:value pairs: an origin vector, a normal vector, and an x-direction vector. Since the plane is just a map, we can access the normal vector using dot syntax: plane.normal

// Structure of a Plane map
var plane = {
  'origin': vector(0,0,0),
  'normal': vector(0,1,0),
  'x': vector(1,0,0)
}

debug(context, plane.normal) // prints vector(0,1,0)
 

Sketch Plane Normal

Back to the task at hand: finding our sketch plane normal. evOwnerSketchPlane() function accepts our queried vertex and returning a Plane object. Since our plane object is just a map, we can access its normal vector using dot syntax: evOwnerSketchPlane().normal.

var normal = evOwnerSketchPlane(context, {
                "entity" : definition.point
        }).normal;

debug(context, normal); // prints vector() with the sketch plane's normal direction
 

Sketch

There are three steps to creating any sketch in FeatureScript:

  1. Create the sketch object
  2. Add geometry to the sketch object
  3. Solve the sketch

Once we've done those three things, our sketch becomes usable to other features in Onshape.

The sketch code in our example is fairly self-explanatory: we create a sketch on a plane, draw a couple of shapes, and solve the sketch. The only important thing to watch out for is the creation of the sketch ID. All features in Onshape should have a unique ID. We'll talk about ID objects in more detail in another tutorial. For now, just know that each feature you create in FeatureScript needs to have a unique ID that looks like this:

// All features in FS should have a unique ID appended to the id object
newSketchOnPlane(context, id + "somethingUnique", definition);

Once we know that, the rest is easy. Sketch operations in FS begin with 'sk', so it's easy to search for them in the code auto-completion.

// create sketch on plane
var theSketch = newSketchOnPlane(context, id + "theSketch", {
        "sketchPlane" : sketchPlane
});

// draw stuff
skRegularPolygon(theSketch, "polygon1", {
        "center" : vector(0, 0) * inch,
        "firstVertex" : vector(0 * inch, definition.od),
        "sides" : 6
});

skCircle(theSketch, "circle1", {
        "center" : vector(0, 0) * inch,
        "radius" : definition.hole / 2
});

// solve sketch
skSolve(theSketch);
 

Extrude

Extruding things is easy, but there are two different extrude tools in FS: 'extrude' and 'opExtrude'. As a best practice, use the latter.

The 'extrude' feature is the one you see in the toolbar. It has lots of extra code in it for working with the UI, and once it's done that it just calls the 'opExtrude' command. That means that 'extrude' is really just an extra layer of icing you don't need on your FeatureScript cake. Rather than use 'extrude'--which just calls 'opExtrude' anyway, it makes more sense to just use opExtrude directly.

// extrude sketch regions created by: id + "theSketch"
// the 'true' parameter tells opExtrude to ignore inner loops, thus
// keeping our circle from being extruded
opExtrude(context, id + "extrude1", {
        "entities" : qSketchRegion(id + "theSketch", true),
        "direction" : normal,
        "endBound" : BoundingType.BLIND,
        "endDepth" : definition.thick
});
 

Cleanup

IT'S ALIVE! Our code works. It creates a Plane object, puts a sketch on that plane, draws some geometry in the sketch, and extrudes the sketch. Awesome.

But the sketch was really just a means to an end. We don't really want it in the final result, we just need it during construction. To get rid of the sketch (and anything else we don't need), we can use the 'opDeleteBodies' operation.

(Note: like the 'extrude' function above, there is a 'deleteBodies' function in Onshape. The same best practice applies to this as to extrude: it's better to use the deeper opDeleteBodies() operation than the higher-level deleteBodies() feature.)

// delete all bodies created by the feature: id + "theSketch"
opDeleteBodies(context, id + "deleteBodies1", {
        "entities" : qCreatedBy(id + "theSketch", EntityType.BODY)
});
 

Parameters

Last we add our UI parameters. This might seem backward, but I find that anything I can do to decrease cognitive load while coding is a good thing: my cognition can only carry so much load! By trying to think about the UI and the guts of the program at the same time, I get confused, frustrated, irritable, and generally unpleasant.

Instead, I prefer to do the simplest possible code, with static values for dimensions. That way I can focus my attention solely on the business logic of the code. Once I've done that, I can backtrack, adding in UI details as needed.

You may find a different method works better for you, and that's okay. My experience has led me to work in a very iterative way, beginning with simple code, augmenting it, and then re-writing it from scratch over and over again. That might seem like a lot of redundant work, but I've found that I get better results that way than trying to anticipate every possible problem in advance. Do whatever works for you.

annotation { "Name" : "Outer Diameter" }
isLength(definition.od, LENGTH_BOUNDS);

annotation { "Name" : "Hole Diameter" }
isLength(definition.hole, LENGTH_BOUNDS);

annotation { "Name" : "Thickness" }
isLength(definition.thick, LENGTH_BOUNDS);

...

skRegularPolygon(theSketch, "polygon1", {
        "center" : vector(0, 0) * inch,
        "firstVertex" : vector(0 * inch, definition.od / 2),
        "sides" : 6
});

skCircle(theSketch, "circle1", {
        "center" : vector(0, 0) * inch,
        "radius" : definition.hole / 2
});

...

opExtrude(context, id + "extrude1", {
        "entities" : qSketchRegion(id + "theSketch", true),
        "direction" : normal,
        "endBound" : BoundingType.BLIND,
        "endDepth" : definition.thick
});
 

Note: Units

Length variables in FS are typically created by multiplying a number by a unit. The implications of this are a bit odd at first, but workable.

In our example, our hexagon's firstVertex parameter defaults to a vector() multiplied by a unit (inch). This works as long as both of the vector values are numbers.

definition.od is not a number, however. It's a length, and you can't multiply a length by a unit. So for this to work, we have to change the structure of the statement.

// before
vector(0, 1) * inch;

// incorrect:
// vector(0, definition.od) * inch

// correct
vector(0 * inch, definition.od / 2);
 

Full Code

In only a few lines of code we create a full-fledged Onshape feature that creates a simple hex nut shape centered on a given sketch vertex, and using a user-supplied outer diameter, hole diameter, and thickness.

A production feature would be more complex, including standard nut sizes, threads, fillets, orientation options, and the ability to place multiple nuts in a single feature. Don't worry, we'll add that stuff later. I find that it's best to work iteratively, beginning with the simplest-possible code and gradually adding complexity.


FeatureScript 355;
import(path : "onshape/std/geometry.fs", version : "355.0");

annotation { "Feature Type Name" : "Nut" }
export const nut = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "Point", "Filter" : EntityType.VERTEX && SketchObject.YES, "MaxNumberOfPicks" : 1 }
        definition.point is Query;
        
        annotation { "Name" : "Outer Diameter" }
        isLength(definition.od, LENGTH_BOUNDS);
        
        annotation { "Name" : "Hole Diameter" }
        isLength(definition.hole, LENGTH_BOUNDS);
        
        annotation { "Name" : "Thickness" }
        isLength(definition.thick, LENGTH_BOUNDS);
    }
    {
        var origin = evVertexPoint(context, {
                "vertex" : definition.point
        });
        
        var normal = evOwnerSketchPlane(context, {
                "entity" : definition.point
        }).normal;
        
        var sketchPlane = plane(origin, normal);
        
        var theSketch = newSketchOnPlane(context, id + "theSketch", {
                "sketchPlane" : sketchPlane
        });
        
        skRegularPolygon(theSketch, "polygon1", {
                "center" : vector(0, 0) * inch,
                "firstVertex" : vector(0 * inch, definition.od / 2),
                "sides" : 6
        });
        
        skCircle(theSketch, "circle1", {
                "center" : vector(0, 0) * inch,
                "radius" : definition.hole / 2
        });
        
        skSolve(theSketch);
        
        opExtrude(context, id + "extrude1", {
                "entities" : qSketchRegion(id + "theSketch", true),
                "direction" : normal,
                "endBound" : BoundingType.BLIND,
                "endDepth" : definition.thick
        });
        
        opDeleteBodies(context, id + "deleteBodies1", {
                "entities" : qCreatedBy(id + "theSketch", EntityType.BODY)
        });
        
    });