Hex Nut Part 2: Dropdown Menu

Pump Up the Presets

In this video we build on last week's hex nut code to add a drop-down list of standard metric nut sizes. We'll begin with a simple example using simple reusable code snippets, and then use that knowledge in our live code.

Full explanation, snippets, and code provided below.


Enum.

First, we need a drop-down menu. An enum (i.e. "enumerated data type") in FeatureScript serves exactly this purpose: we define a bunch of values, and those values appear as options in the dropdown.

Note that there are two parts to each entry: the annotation containing the pretty name that will be displayed in the UI, and an all-caps internal name that will be used in our code. (The annotation is optional.)

Note: There is no shame in copy/paste coding, especially as a beginner. For what it's worth, I use snippets exactly like the ones below in my own work, and I recommend you do the same.

export enum MyEnum
{
    annotation { "Name" : "One" }
    ONE,
    annotation { "Name" : "Two" }
    TWO,
    annotation { "Name" : "Three" }
    THREE
}

With this code in place, all we have to do is create a new feature and add an enum parameter from the parameter snippets menu.

annotation { "Feature Type Name" : "My Feature" }
export const myFeature = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "My Enum" }
        definition.myEnum is MyEnum;
    }
    {
        //feature logic
    });

Hooray! We now have a drop-down menu. Play around with this a bit. Try adding new items to the menu, and modifying existing ones. See if you can break it.

Make it Function

Much like Adam himself, our drop-down menu is pretty, but useless. We need to equate a menu selection with an actual length value for modeling purposes.

To do that, we'll use a type of subroutine called a function. A function is just a piece of reusable logic that we can call whenever we need it. In this case, our function accepts a value of type MyEnum as input, and returns a value based on that.

This one, I admit, get's a bit programmy. Rather than delve into the intricacies of this function, for now we'll just copy/paste it into our code, just below the MyEnum declaration at the top.

Note that the keys in the return map correspond exactly with the names of values in MyEnum. This is crucial.

export function evMyEnum(value is MyEnum)
{
    return {
            "ONE" : 1 * centimeter,
            "TWO" : 2 * centimeter,
            "THREE" : 3 * centimeter
        }[value as string];
}

Full Template

Add in a simple cube, and we have a fully functioning feature. Give this code a play and see what you can do.

Can you add "Four" to the drop-down? Can you make it function?

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

export enum MyEnum
{
    annotation { "Name" : "One" }
    ONE,
    annotation { "Name" : "Two" }
    TWO,
    annotation { "Name" : "Three" }
    THREE
}

export function evMyEnum(value is MyEnum)
{
    return {
            "ONE" : 1 * centimeter,
            "TWO" : 2 * centimeter,
            "THREE" : 3 * centimeter
        }[value as string];
}

annotation { "Feature Type Name" : "My Feature" }
export const myFeature = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "My Enum" }
        definition.myEnum is MyEnum;
        
    }
    {
        cube(context, id, {
                "sideLength" : evMyEnum(definition.myEnum)
        });
    });

Defining the Standard

Now that we understand the theory, let's add it to our live code.

First we add our enum declaration with all of the various options. Easy-peasy.

export enum BoltMetric
{
    annotation { "Name" : "M1.6" }
    M1_6,
    annotation { "Name" : "M2" }
    M2,
      //...
    annotation { "Name" : "Custom" }
    CUSTOM
}

Next we need a function that relates those keys to values. This one is also fairly simple, but a bit more complex than the original example.

First, I'm declaring a constant equating 'mm' to 'millimeter', essentially creating a shorthand for myself. I would hate to have to type out "millimeter" four times on every line of this snippet!

Second, the values we're returning are actually maps (wrapped in curly braces) returning a bunch of key:value pairs. If we choose an M2 bolt, for example, we need not only the hole size, but also the pitch, outer diameter, and thickness. We can store all of those in a single map, return it, and access those parts later on as needed.

export function evBoltMetric(value is BoltMetric)
{
    const mm = millimeter;
    return {
            "M1_6" : { "hole" : 1.6*mm, "pitch" : 0.35*mm, "od" : 3.41*mm, "thick" : 1.3*mm },
            "M2" : { "hole" : 2*mm, "pitch" : 0.4*mm, "od" : 4.32*mm, "thick" : 1.6*mm },
      //...
            "M64" : { "hole" : 64*mm, "pitch" : 6*mm, "od" : 104.86*mm, "thick" : 51*mm }
        }[value as string];
}

Conditional Parameters

And remember, we only need to show our explicit dimensions if the drop-down is on "Custom" mode. Otherwise we'll just use the values defined in our standard. There are two parts to this: one in the precondition, and one in the body of the feature.

First, let's make it so that our custom parameters only show up in the UI if we're in Custom mode:

 precondition
    {
        //...
        annotation { "Name" : "Size" }
        definition.size is BoltMetric;
        
        if (definition.size == BoltMetric.CUSTOM){
            // display custom parameters
        }
    }

Now we just have to wire the proper values into the modeling code itself.

var dims;
        
if (definition.size == BoltMetric.CUSTOM){
  dims = definition;
}
else {
  dims = evBoltMetric(definition.size);
}

//...

skRegularPolygon(hex, "hexagon1", {
  "center" : vector(0, 0) * inch,
  "firstVertex" : vector(0 * inch, dims.od / 2),
  "sides" : 6
});

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

//...

opExtrude(context, id + "extrude1", {
  "entities" : qSketchRegion(id + "hexSketch", false),
  "direction" : plane.normal,
  "endBound" : BoundingType.BLIND,
  "endDepth" : dims.thick
});

Full Code

Sweet cuppin' cakes, we've built our little selves a drop-down menu. Purdy cool, right?

I highly recommend spending some time hacking at this code. Make some changes. Re-order things. Rename things. Remove things. Add things. Break it. Fix it. Get comfortable with how this code operates, because from this point forward I'm going to assume that you know how to do it.

Have fun.

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

export enum BoltMetric
{
    annotation { "Name" : "M1.6" }
    M1_6,
    annotation { "Name" : "M2" }
    M2,
    annotation { "Name" : "M2.5" }
    M2_5,
    annotation { "Name" : "M3" }
    M3,
    annotation { "Name" : "M4" }
    M4,
    annotation { "Name" : "M5" }
    M5,
    annotation { "Name" : "M6" }
    M6,
    annotation { "Name" : "M8" }
    M8,
    annotation { "Name" : "M10" }
    M10,
    annotation { "Name" : "M12" }
    M12,
    annotation { "Name" : "M14" }
    M14,
    annotation { "Name" : "M16" }
    M16,
    annotation { "Name" : "M20" }
    M20,
    annotation { "Name" : "M24" }
    M24,
    annotation { "Name" : "M30" }
    M30,
    annotation { "Name" : "M36" }
    M36,
    annotation { "Name" : "M42" }
    M42,
    annotation { "Name" : "M48" }
    M48,
    annotation { "Name" : "M56" }
    M56,
    annotation { "Name" : "M64" }
    M64,
    annotation { "Name" : "Custom" }
    CUSTOM
}

export function evBoltMetric(value is BoltMetric)
{
    const mm = millimeter;
    return {
            "M1_6" : { "hole" : 1.6*mm, "pitch" : 0.35*mm, "od" : 3.41*mm, "thick" : 1.3*mm },
            "M2" : { "hole" : 2*mm, "pitch" : 0.4*mm, "od" : 4.32*mm, "thick" : 1.6*mm },
            "M2_5" : { "hole" : 2.5*mm, "pitch" : 0.45*mm, "od" : 5.45*mm, "thick" : 2*mm },
            "M3" : { "hole" : 3*mm, "pitch" : 0.5*mm, "od" : 6.01*mm, "thick" : 2.4*mm },
            "M4" : { "hole" : 4*mm, "pitch" : 0.7*mm, "od" : 7.66*mm, "thick" : 3.2*mm },
            "M5" : { "hole" : 5*mm, "pitch" : 0.8*mm, "od" : 8.79*mm, "thick" : 4.7*mm },
            "M6" : { "hole" : 6*mm, "pitch" : 1*mm, "od" : 11.05*mm, "thick" : 5.2*mm },
            "M8" : { "hole" : 8*mm, "pitch" : 1.25*mm, "od" : 14.38*mm, "thick" : 6.8*mm },
            "M10" : { "hole" : 10*mm, "pitch" : 1.5*mm, "od" : 17.77*mm, "thick" : 8.4*mm },
            "M12" : { "hole" : 12*mm, "pitch" : 1.75*mm, "od" : 20.03*mm, "thick" : 10.8*mm },
            "M14" : { "hole" : 14*mm, "pitch" : 2*mm, "od" : 23.35*mm, "thick" : 12.8*mm },
            "M16" : { "hole" : 16*mm, "pitch" : 2*mm, "od" : 26.75*mm, "thick" : 14.8*mm },
            "M20" : { "hole" : 20*mm, "pitch" : 2.5*mm, "od" : 32.95*mm, "thick" : 18*mm },
            "M24" : { "hole" : 24*mm, "pitch" : 3*mm, "od" : 39.55*mm, "thick" : 21.5*mm },
            "M30" : { "hole" : 30*mm, "pitch" : 3.5*mm, "od" : 50.85*mm, "thick" : 25.6*mm },
            "M36" : { "hole" : 36*mm, "pitch" : 4*mm, "od" : 60.78*mm, "thick" : 31*mm },
            "M42" : { "hole" : 42*mm, "pitch" : 4.5*mm, "od" : 71.3*mm, "thick" : 34*mm },
            "M48" : { "hole" : 48*mm, "pitch" : 5*mm, "od" : 82.6*mm, "thick" : 38*mm },
            "M56" : { "hole" : 56*mm, "pitch" : 5.5*mm, "od" : 93.56*mm, "thick" : 45*mm },
            "M64" : { "hole" : 64*mm, "pitch" : 6*mm, "od" : 104.86*mm, "thick" : 51*mm }
        }[value as string];
}


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 }
        definition.point is Query;
        
        annotation { "Name" : "Size" }
        definition.size is BoltMetric;
        
        if (definition.size == BoltMetric.CUSTOM){
        
            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 dims;
        
        if (definition.size == BoltMetric.CUSTOM){
            dims = definition;
        }
        else {
            dims = evBoltMetric(definition.size);
        }
        
        var normal = evOwnerSketchPlane(context, {
                "entity" : definition.point
        }).normal;
        
        var origin = evVertexPoint(context, {
                "vertex" : definition.point
        });
        
        var plane = plane(origin, normal);
        
        var hex = newSketchOnPlane(context, id + "hexSketch", {
                "sketchPlane" : plane
        });
        
        skRegularPolygon(hex, "hexagon1", {
                "center" : vector(0, 0) * inch,
                "firstVertex" : vector(0 * inch, dims.od / 2),
                "sides" : 6
        });
        
        skCircle(hex, "circle1", {
                "center" : vector(0, 0) * inch,
                "radius" : dims.hole / 2
        });
        
        skSolve(hex);
        
        opExtrude(context, id + "extrude1", {
                "entities" : qSketchRegion(id + "hexSketch", false),
                "direction" : plane.normal,
                "endBound" : BoundingType.BLIND,
                "endDepth" : dims.thick
        });
        
        opDeleteBodies(context, id + "deleteBodies1", {
                "entities" : qCreatedBy(id + "hexSketch", EntityType.BODY)
        });
    });