Welcome to FeatureScript
FeatureScript guide
Language reference

Feature UI

The user interface to both custom and standard features is a feature dialog. This page describes methods to create the inputs to these feature dialogs, and two alternative ways of updating the parameters dynamically.

Defining a feature dialog

Onshape creates the feature dialog by doing a static analysis (rather than an execution) of the feature declaration, primarily of the precondition. If the feature declaration does not match the allowed form, warnings are reported and the feature is not made available to Part Studios. A minimal valid feature declaration is:

annotation { "Feature Type Name" : "Fancy Fillet" /* May have additional options */}
export const fancyFillet = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        // Parameter specifications go here
    }
    {
        // Feature Body
    });

This declaration can be easily extended in three places: The feature annotation, the feature precondition, and the feature body.

Feature annotations

A constant needs a "Feature Type Name" to be processed as a feature. Additionally, the following other fields can optionally be specified in the annotation map (with strings or arrays of strings):

Parameters

The parameter specification simultaneously serves as a validity check for the feature and a description of the UI for inputting that parameter. Any parameter specification may be annotated with a "Name", specifying the user-facing name of the parameter.

Parameter specifications may be factored out into predicates (e.g. usages of booleanStepScopePredicate in std). Parameters may also be made conditional by putting them in if statements.

Code snippets for the parameter specifications described in this section are available inside a Feature Studio in the parameter snippets menu:

Parameter snippets menu

Booleans

The specification of a boolean parameter in the precondition uses the is operator on the indicated definition field:

annotation { "Name" : "My Boolean" }
definition.myBoolean is boolean;
Boolean in feature dialog

The boolean parameter is rendered as a checkbox by default (but may be changed with a UIHint).

Strings

A string parameter is specified similarly:

annotation { "Name" : "My String" }
definition.myString is string;
String in feature dialog

This creates a text box, whose input can be any FeatureScript string.

Enums

An enum parameter is specified using the is expression, and must refer to an enum that this module exports. Each individual enumeration value may have a user-visible "Name" annotation:

export enum MyOption
{
    annotation { "Name" : "Option One" }
    ONE,
    annotation { "Name" : "Option Two" }
    TWO
}
...
annotation { "Name" : "My Enum" }
definition.myEnum is MyOption;
Enum in feature dialog

An enum parameter is rendered as a dropdown menu with an option for every enum value.

An enumeration value may also be annotated with "Hidden" : true, which will hide a no-longer-supported value from end users.

Quantities

A quantity parameter represents a value with units. For instance, a one-dimensional length parameter may be specified with an isLength predicate:

annotation { "Name" : "My Length" }
isLength(definition.myLength, LENGTH_BOUNDS);
Length in feature dialog

A quantity is rendered as a text box into which a number, a number with units, or a mathematical expression may be typed.

We currently support the following quantity and predicates to specify them:

isAnything allows any expression with any units or even any FeatureScript type (though the UI currently blocks some types). The other predicates require a bounds argument, which also specifies the default value (see the value bounds module for more info).

Queries

When the user selects entities (vertices, edges, faces, and bodies), they are passed to the feature as a query. The minimal query parameter specification is definition.myParameter is Query;. There are two query-specific annotations allowed that restrict what can be selected:

Note that the feature should not rely on the queries satisfying the filter or evaluating to at most MaxNumberOfPicks because changes to upstream features may change the type of the selected entity or turn a single selected entity into multiple.

The "Filter" expression can contain references to the following enums:

These references may be combined using &&, ||, and !, even though such an expression could not be evaluated outside of an annotation. For example, to allow only at most two sketch circles or arcs, use:

annotation { "Name" : "Round things",
             "Filter" : (GeometryType.CIRCLE || GeometryType.ARC) && SketchObject.YES,
             "MaxNumberOfPicks" : 2 }
definition.roundThings is Query;

Query in feature dialog

These filters are only applied on the client during selection; because of changes to features earlier in the feature list, the actual query passed in may evaluate to entities that do not actually pass the filters, so it is up to the feature author to check or filter the input inside the feature body. Some filters like EdgeTopology do not yet have corresponding queries.

Lookup tables

A lookup table is a UI built around specifying a parameter (or set of parameters) though a decision tree. This is similar to selecting an option from an enum value, but each option can have additional sub-options, each sub-option may have sub-sub-options, etc. A typical use is navigating to a standard size, which you can try out in Onshape's Hole feature.

An example of a feature using a lookup table is below:

const ml is ValueWithUnits = centimeter ^ 3;
const oz is ValueWithUnits = 29.5735 * ml;

export const sizeTable = {
        "name" : "region",
        "displayName" : "Region",
        "default" : "EU",
        "entries" : {
            "EU" : {
                "name" : "size",
                "displayName" : "Size",
                "default" : "Medium",
                "entries" : {
                    "Small" : 250 * ml,
                    "Medium" : 350 * ml,
                    "Large" : 500 * ml
                }
            },
            "US" : {
                "name" : "size",
                "displayName" : "Size",
                "default" : "Large",
                "entries" : {
                    "Medium" : 16 * oz,
                    "Large" : 32 * oz,
                    "Supersize" : 64 * oz
                }
            }
        }
    };

annotation { "Feature Type Name" : "Soda" }
export const soda = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "Soda size", "Lookup Table" : sizeTable }
        definition.size is LookupTablePath;
    }
    {
        var size is ValueWithUnits = getLookupTable(sizeTable, definition.size);

        fCylinder(context, id + "cylinder1", {
                "bottomCenter" : vector(0, 0, 0) * inch,
                "topCenter" : vector(0, 0, 1) * size ^ (1/3),
                "radius" : size ^ (1/3) / sqrt(PI)
        });
    });

Lookup table parameter

A lookup table is a tree data structure, defined as a FeatureScript map with a specific format. The root node of the tree specifies the FeatureScript "name" of the option (in the above example, "region"), the "displayName" of the option to show in the dialog (above, "Region"), and, optionally, the "default" value of the option (above, "EU").

The root node finally specifies a map of "entries". The key of each entry is the user-visible name of that option. The value of each entry may be one of two things:

  1. If more choices are needed, the entry value will be the next node of the decision tree, with the same form as the root node: a map of "name", "displayName", optional "default", and another map of "entries".
  2. If no more are choices needed, the entry value will be final value of the parameter (above, the volumes of e.g. 250 * ml).

Successive nodes of the decision tree may be nested arbitrarily deep, but every possible path should terminate in a final value.

The parameter's final value may be of any form. One lookup table can specify the value of multiple parameters by setting each final value to a map of several parameters.

A parameter based on the lookup table is declared with type LookupTablePath and annotated with its associated "LookupTable". The LookupTablePath given in the definition can then be resolved to the final value of the parameter with the getLookupTable function.

Arrays

An array parameter contains a FeatureScript array of items, where each item contains one or more inner parameters. The inner parameters may be of any parameter type (though one array parameter cannot be nested inside another). A user can add any number of items to the array parameter, and may click on the item names to expand or collapse the parameters within.

annotation { "Name" : "Profiles",
    "Item name" : "Profile",
    "Item label template" : "Profile (#width)" }
definition.profiles is array;

for (var profile in definition.profiles)
{
    annotation { "Name" : "Width" }
    isLength(profile.width, LENGTH_BOUNDS);

    annotation { "Name" : "Make square" }
    profile.isSquare is boolean;
}
Array parameter feature dialog

The value of an array parameter is an array of maps, e.g.:

[
    { "width" : 0.9 * inch, "isSquare" : false },
    { "width" : 1.2 * inch, "isSquare" : true }
]

As with any FeatureScript array, the items inside can be accessed (via definition.profiles[0] ) or iterated through (via for (var profile in definition.profiles) {} ).

The annotation for an array parameter can include the following fields:

Additionally, the UIHint "MATCH_LAST_ARRAY_ITEM" can be used on an inner parameter to set its default value according to the last item in the current list, if it exists.

Note that inner parameter names must be unique across the entire feature (even though they are not stored directly on the definition map).

UIHints

A parameter may have one or more "UIHint" strings specified in the annotation. For example, OPPOSITE_DIRECTION causes the boolean to be rendered as an opposite direction button, typically used in tandem with a length parameter:

annotation { "Name" : "Flip", "UIHint" : "OPPOSITE_DIRECTION" }
definition.isFlipped is boolean;
Opposite direction in feature dialog

Most UIHints are type-specific, but any parameter may have an "ALWAYS_HIDDEN" UIHint, to prevent it from ever appearing in the dialog. This can be useful for keeping hidden state in the definition for use by manipulators or editing logic.

Conditional visibility

Some parameters only make sense for certain settings of other parameters. To express this, the specifications for conditionally visible parameters can be put in if statements referencing already specified parameters. For example:

    ...
    precondition
    {
        definition.useCustomLength is boolean;
        if (definition.useCustomLength)
            isLength(definition.customLength, LENGTH_BOUNDS);
    }
    ...

The conditions for the if statements may include comparisons of prior boolean and enum parameters to fixed values using == and !=. They may also include the logical operators &&, ||, and !. The if statements may be nested.

A current limitation is that a parameter specification can only appear once per feature, and so, for example, cannot appear with different annotations in different branches of an if statement. If this behavior is desired, we recommend you use two different parameter names, and merge them inside the function.

Manipulator change function

The manipulator change function allows you to specify manipulator behavior in a very flexible way. It is typically declared as:

export function onManipulatorChange(context is Context, definition is map, newManipulators is map) returns map
{
    // modify the definition based on what's in newManipulators
    ...

    return definition;
}

The idea is that when the feature regenerates, it defines some manipulators using addManipulators to be shown on the screen. When a user drags one of these manipulators, the manipulator change function gets called. It can look at the context, the feature definition, and the manipulator changes, and change the feature definition in response. Thus, a manipulator does not have to be tied to a particular parameter and could modify a group of parameters all at once.

See the manipulator module for a simple example.

Manipulator change function arguments

Manipulator change function return value

The return value is a map like definition with some of the parameters modified. The parameter types that can be changed are:

Editing logic function

If an editing logic function is specified, it is called whenever the feature definition is changed either explicitly by the user or with a manipulator (but see isCreating in arguments). It can make additional feature definition changes based on the context and the users' edits. The editing logic function is primarily intended for setting default parameters in an intelligent manner.

For example, the modifyFillet feature in std/modifyFillet.fs uses it to set the fillet radius based on the current radius of the selected fillet. The extrude feature uses it to set the opposite direction parameter as well as the boolean merge scope. The cPlane feature uses it to set the plane type based on the preselection.

A word of caution: it is easy to abuse the editing logic function to create a user experience that is more annoying than helpful. Enjoy editing logic responsibly.

Editing logic function arguments

An editing logic function may have between four and seven arguments. A declaration with seven arguments looks like:

export function onFeatureChange(context is Context, id is Id, oldDefinition is map, definition is map,
                                isCreating is boolean, specifiedParameters is map, hiddenBodies is Query) returns map
{
    // modify the definition
    ...

    return definition;
}

The first four arguments are required.

Any (or all) of the remaining three arguments may be omitted, but those that remain must appear in the same order relative to each other. Omitting unused arguments may result in better performance.

Editing logic function return value

The return value is a map following the same rules as the return value for the manipulator change function.