接續上一篇文章, 整理目前 Onshape 已經釋出的 FeatureScript 相關應用範例.

Curve Pattern FeatureScript 程式 Document

Helix in Fill FeatureScript 程式 Document

3D Spline Fill FeatureScript 程式 Document

Brick FeatureScript 程式 Document

Measure Distance FeatureScript 程式 Document

Lighten FeatureScript 程式 Document

Snap Hook FeatureScript 程式原始碼:

/*    
    Snap Hook

    This custom feature creates a common fastening feature in plastic part design.

    The Snap Hook is just one version of this type of fastening feature 
    and could be easily extended to include many other types. This was built
    to show that you can create complex, compound features easily.

    Version 1 - April 26, 2016 - Neil Cooke, Onshape Inc.
*/ 

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

annotation { "Feature Type Name" : "Snap Hook" }
export const SnapHook = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "Sketch point locations", "Filter" : EntityType.VERTEX && SketchObject.YES && ConstructionObject.NO }
        definition.locations is Query;

        annotation { "Name" : "Height type" }
        definition.style is HookStyle;

        if (definition.style == HookStyle.BLIND)
        {
            annotation { "Name" : "Height", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.height, HOOK_HEIGHT);
        }
        else
        {
            annotation { "Name" : "Parallel face or plane", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
            definition.parallelFace is Query;
        }

        annotation { "Name" : "Width", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
        isLength(definition.hookWidth, HOOK_WIDTH);

        annotation { "Name" : "Flip direction", "UIHint" : "OPPOSITE_DIRECTION" }
        definition.hookFlipDirection is boolean;

        annotation { "Name" : "Edge to define direction", "Filter" : EntityType.EDGE, "MaxNumberOfPicks" : 1 }
        definition.hookDirection is Query;

        annotation { "Name" : "Thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
        isLength(definition.hookThickness, HOOK_THK);

        annotation { "Name" : "Undercut depth", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
        isLength(definition.hookDepth, HOOK_THK);

        annotation { "Name" : "Lip height", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
        isLength(definition.flatHeight, HOOK_LIP);

        annotation { "Name" : "Insertion angle", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
        isAngle(definition.deflectionAngle, HOOK_ANGLE);

        annotation { "Name" : "Draft", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"], "Default" : true }
        definition.hasDraft is boolean;

        if (definition.hasDraft == true)
        {
            annotation { "Name" : "Draft angle", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"] }
            isAngle(definition.draftAngle, ANGLE_STRICT_90_BOUNDS);

            annotation { "Name" : "Back face draft angle", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isAngle(definition.backDraftAngle, ANGLE_STRICT_90_BOUNDS);
        }
        annotation { "Name" : "Cutout", "Default" : true }
        definition.hasCutout is boolean;

        annotation { "Name" : "Merge scope", "Filter" : EntityType.BODY && BodyType.SOLID }
        definition.booleanScope is Query;
    }

    {
        // get all the user selected locations
        const locations = evaluateQuery(context, definition.locations);

        // if a solid body intersects the first point in the list, automatically use that in the merge scope
        const targetBody = evaluateQuery(context, qContainsPoint(qBodyType(qEverything(EntityType.BODY), BodyType.SOLID), evVertexPoint(context, { "vertex" : locations[0] })));

        if (size(targetBody) == 0 && definition.booleanScope != undefined)
            definition.targetBody = definition.booleanScope; // if not, get user to select merge scope
        else
            definition.targetBody = targetBody[0];

        var sketchPlane is Plane = evOwnerSketchPlane(context, { "entity" : locations[0] });
        var topPlane;

        var hookVector = vector(1, 0); // by default pointing across in x

        // if user has defined hook direction, work out the vector
        if (definition.hookDirection != undefined)
        {
            const directionResult = try(evAxis(context, { "axis" : definition.hookDirection }));

            if (directionResult != undefined)
                hookVector = normalize(vector(directionResult.direction[0], directionResult.direction[1]));
        }

        if (definition.hookFlipDirection)
            hookVector = hookVector * -1;

        // get vector perpendicular to hook direction
        var perpHookVector = vector(hookVector[1] * -1, hookVector[0]);

        // define the plane for the top of the boss
        if (definition.style == HookStyle.PLANE && definition.parallelFace != undefined)
            topPlane = evPlane(context, { "face" : definition.parallelFace });
        else
            topPlane = plane(sketchPlane.origin + definition.height * sketchPlane.normal, sketchPlane.normal);

        var nameId = 1;
        var chamferPoints = [];
        var frontFacePoints = [];
        var backFacePoints = [];

        const sketch1 = newSketchOnPlane(context, id + "sketch1", { "sketchPlane" : topPlane });
        const sketch2 = newSketchOnPlane(context, id + "sketch2", { "sketchPlane" : topPlane });
        const sketch3 = newSketchOnPlane(context, id + "sketch3", { "sketchPlane" : topPlane });

        definition.depth = definition.hookDepth / tan(definition.deflectionAngle) + definition.flatHeight;

        // Build 3 sketches each with a rectangle
        for (var location in locations)
        {
            var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location }));

            skRectangle(sketch1, "rectangleHook" ~ nameId, {
                        "firstCorner" : vector(point[0], point[1]) + (definition.hookWidth / 2) * hookVector,
                        "secondCorner" : vector(point[0], point[1]) - (definition.hookWidth / 2) * hookVector - definition.hookDepth * perpHookVector
                    });

            skRectangle(sketch2, "rectangleThickness" ~ nameId, {
                        "firstCorner" : vector(point[0], point[1]) - (definition.hookWidth / 2) * hookVector,
                        "secondCorner" : vector(point[0], point[1]) + (definition.hookWidth / 2) * hookVector + definition.hookThickness * perpHookVector
                    });

            skRectangle(sketch3, "completeRectangle" ~ nameId, {
                        "firstCorner" : vector(point[0], point[1]) - (definition.hookWidth / 2) * hookVector - definition.hookDepth * perpHookVector,
                        "secondCorner" : vector(point[0], point[1]) + (definition.hookWidth / 2) * hookVector + definition.hookThickness * perpHookVector
                    });

            // Keep a list of the centerpoints of the edges where the chamfers may go
            var chamferPoint2d = vector(point[0], point[1]) - definition.hookDepth * perpHookVector;
            if (definition.hasDraft)
            {
                chamferPoint2d = vector(point[0], point[1]) - (definition.hookDepth - definition.depth * tan(definition.draftAngle)) * perpHookVector;
            }
            chamferPoints = append(chamferPoints, toWorld(planeToCSys(topPlane), vector(chamferPoint2d[0], chamferPoint2d[1], definition.depth)));

            var backFacePoint2d = vector(point[0], point[1]) + definition.hookThickness * perpHookVector;
            backFacePoints = append(backFacePoints, toWorld(planeToCSys(topPlane), vector(backFacePoint2d[0], backFacePoint2d[1], 0 * meter)));
            frontFacePoints = append(frontFacePoints, toWorld(planeToCSys(topPlane), vector(point[0], point[1], 0 * meter)));

            nameId += 1;
        }
        skSolve(sketch1);
        skSolve(sketch2);
        skSolve(sketch3);

        extrude(context, id + ("extrude1"), {
                    "entities" : qSketchRegion(id + "sketch2"),
                    "endBound" : BoundingType.UP_TO_BODY,
                    "depth" : definition.depth,
                    "endBoundEntityBody" : definition.targetBody,
                    "oppositeDirection" : true,
                    "hasDraft" : definition.hasDraft,
                    "draftAngle" : definition.draftAngle,
                    "draftPullDirection" : false,
                    "operationType" : NewBodyOperationType.ADD,
                    "defaultScope" : false,
                    "booleanScope" : definition.targetBody
                });

        extrude(context, id + ("extrude2"), {
                    "entities" : qSketchRegion(id + "sketch3"),
                    "endBound" : BoundingType.BLIND,
                    "depth" : definition.depth,
                    "endBoundEntityBody" : definition.targetBody,
                    "oppositeDirection" : false,
                    "hasDraft" : definition.hasDraft,
                    "draftAngle" : definition.draftAngle,
                    "draftPullDirection" : true,
                    "operationType" : NewBodyOperationType.ADD,
                    "defaultScope" : false,
                    "booleanScope" : definition.targetBody
                });

        var chamferEdges = [];

        for (var i = 0; i < size(chamferPoints); i += 1)
        {
            // Find the edges that intersect the points previously collected
            chamferEdges = append(chamferEdges, qContainsPoint(qCreatedBy(id + "extrude2", EntityType.EDGE), chamferPoints[i]));
        }

        try(opChamfer(context, id + "chamfer1", {
                        "entities" : qUnion(chamferEdges),
                        "chamferType" : ChamferType.OFFSET_ANGLE,
                        "width" : definition.depth - definition.flatHeight,
                        "angle" : definition.deflectionAngle,
                        "oppositeDirection" : true
                    }));

        if (definition.hasDraft)
        {
            var backFaces = [];
            var frontFaces = [];

            for (var i = 0; i < size(backFacePoints); i += 1)
            {
                // Find the edges that intersect the points previously collected
                backFaces = append(backFaces, qContainsPoint(qCreatedBy(id + "extrude1", EntityType.FACE), backFacePoints[i]));
                frontFaces = append(frontFaces, qContainsPoint(qCreatedBy(id + "extrude1", EntityType.FACE), frontFacePoints[i]));
            }

            opPlane(context, id + "plane1", {
                        "plane" : topPlane,
                        "size" : 0.1 * meter
                    });

            opDraft(context, id + "draft1", {
                        "neutralPlane" : qCreatedBy(id + "plane1", EntityType.FACE),
                        "pullVec" : topPlane.normal,
                        "draftFaces" : qUnion(frontFaces),
                        "angle" : 0 * degree
                    });

            opDraft(context, id + "draft2", {
                        "neutralPlane" : qCreatedBy(id + "plane1", EntityType.FACE),
                        "pullVec" : topPlane.normal,
                        "draftFaces" : qUnion(backFaces),
                        "angle" : definition.backDraftAngle + definition.draftAngle
                    });
        }

        if (definition.hasCutout)
        {
            extrude(context, id + ("extrude3"), {
                        "entities" : qSketchRegion(id + "sketch1"),
                        "endBound" : BoundingType.THROUGH_ALL,
                        "depth" : definition.depth,
                        "endBoundEntityBody" : definition.targetBody,
                        "oppositeDirection" : true,
                        "hasDraft" : definition.hasDraft,
                        "draftAngle" : definition.draftAngle,
                        "draftPullDirection" : false,
                        "operationType" : NewBodyOperationType.REMOVE,
                        "defaultScope" : false,
                        "booleanScope" : definition.targetBody
                    });
        }

        // Remove sketch entities and plane - no longer required
        var sketches = [qCreatedBy(id + "sketch1"), qCreatedBy(id + "sketch2"), qCreatedBy(id + "sketch3"), qCreatedBy(id + "plane1")];
        opDeleteBodies(context, id + "delete", { "entities" : qUnion(sketches) });

    }, {});

const HOOK_ANGLE =
{
            "min" : -TOLERANCE.zeroAngle * radian,
            "max" : (2 * PI + TOLERANCE.zeroAngle) * radian,
            (degree) : [15, 30, 60]
        } as AngleBoundSpec;

const HOOK_HEIGHT =
{
            "min" : -TOLERANCE.zeroLength * meter,
            "max" : 500 * meter,
            (meter) : [1e-5, 0.015, 500],
            (centimeter) : 1.5,
            (millimeter) : 15.0,
            (inch) : 0.6
        } as LengthBoundSpec;

const HOOK_WIDTH =
{
            "min" : -TOLERANCE.zeroLength * meter,
            "max" : 500 * meter,
            (meter) : [1e-5, 0.005, 500],
            (centimeter) : 0.5,
            (millimeter) : 5.0,
            (inch) : 0.2
        } as LengthBoundSpec;

const HOOK_THK =
{
            "min" : -TOLERANCE.zeroLength * meter,
            "max" : 500 * meter,
            (meter) : [1e-5, 0.002, 500],
            (centimeter) : 0.2,
            (millimeter) : 2.0,
            (inch) : 0.08
        } as LengthBoundSpec;

const HOOK_LIP =
{
            "min" : -TOLERANCE.zeroLength * meter,
            "max" : 500 * meter,
            (meter) : [1e-5, 0.001, 500],
            (centimeter) : 0.1,
            (millimeter) : 1.0,
            (inch) : 0.04
        } as LengthBoundSpec;

export enum HookStyle
{
    annotation { "Name" : "Blind" }
    BLIND,
    annotation { "Name" : "Up to face" }
    PLANE
}

Fill Pattern FeatureScript 程式碼:

FeatureScript 336;
import(path : "onshape/std/geometry.fs", version : "336.0");
import(path : "onshape/std/transform.fs", version : "336.0");

/**
 * Performs a pattern of faces within a face. The instances are placed in a hexagonal pattern and no instances will be 
 * created that cross the boundary of the face. If a border is set then no instances are created within a border of that size
 * @param definition {{
 *      @field entities A collection of faces that will be patterned
 *      @field target A planar face that contains the 'entities' faces and that will contain the pattern
 *      @field direction Specifies the alignment of the pattern in the face
 *      @field distance The distance between the center of the instances
 *      @field border The width of the "exclusion zone" at the edge of the target face     
 * }}
 */
annotation { "Feature Type Name" : "Fill pattern", "Filter Selector" : "allparts" }
export const fillPattern = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "Faces to pattern",
                     "Filter" : EntityType.FACE && ConstructionObject.NO && SketchObject.NO }
        definition.entities is Query;
        annotation { "Name" : "Target face", "Filter" : GeometryType.PLANE, "MaxNumberOfPicks" : 1 }
        definition.target is Query;
        annotation { "Name" : "Direction", "Filter" : QueryFilterCompound.ALLOWS_AXIS, "MaxNumberOfPicks" : 1 }
        definition.direction is Query;        
        annotation { "Name" : "Distance" }
        isLength(definition.distance, LENGTH_BOUNDS);
        annotation { "Name" : "Border" }
        isLength(definition.border, NONNEGATIVE_ZERO_DEFAULT_LENGTH_BOUNDS);
    }
    {
        var transforms = [];
        var instanceNames = [];

        var direction = try(evAxis(context, { "axis" : definition.direction })).direction;
        println(direction);
        var normal = try(evPlane(context, {
            "face" : definition.target
        })).normal;

        // For a hexagonal pattern we have two directions, with one being at an angle of 60 degrees from the other.
        // If we pattern in both those directions then we get a hexagonal pattern with equal spacing
        var vertical = cross(normal, direction);
        var angled = (direction * cos(60 * degree)) + (vertical * sin(60 * degree));

        // We want to get the edges of the target face so that we can get the distance from the face boundary
        // However, because the tool faces ought to be "in" the target face we don't want to count those
        // We can use booleans on the queries, which is pretty nice. We can also take the opportunity to ensure
        // that at least some edges are shared
        var allTargetEdges = qEdgeAdjacent(definition.target, EntityType.EDGE);
        var toolEdges = qEdgeAdjacent(definition.entities, EntityType.EDGE);
        var targetEdges = qSubtraction(allTargetEdges, toolEdges);
        var edgesInFace = qIntersection([allTargetEdges, toolEdges]);
        if (size(evaluateQuery(context, edgesInFace)) == 0) {
            throw regenError("The entities must share edges with the target face");
        }

        // To prevent excessive calculation we do a quick exclusion of faces based on bounding boxes
        // It doesn't need to be perfect. Get the box of the target face and of the shared edges
        var faceBox = try(evBox3d(context, {
            "topology" : definition.target
        }));
        var toolBox = try(evBox3d(context, {
            "topology" : edgesInFace
        }));

        // Again, to prevent excessive computation we will estimate the maximum number of instances and
        // fail early if we exceed some limit.
        var diagonal = faceBox.maxCorner - faceBox.minCorner;    
        var maximumIndex = round((norm(diagonal) - (definition.border * 2)) / definition.distance);
        var toolSize = norm(toolBox.maxCorner - toolBox.minCorner);
        var estimatedIndexCount = maximumIndex * maximumIndex;
        if (estimatedIndexCount > 2500)
        {
            throw regenError("Too many instances in the pattern (estimate: " ~ 
                estimatedIndexCount ~ " ). Try a larger spacing.");
        }

        // Now we loop and look to see if the instance should be included
        for (var i = -maximumIndex; i <= maximumIndex; i += 1)
        {
            for (var j = -maximumIndex; j <= maximumIndex; j += 1)
            {
                if (abs(i) < 0.5 && abs(j) < 0.5)
                {
                    // Zero transform = initial position => Skip
                    continue;
                }
                var translation = ((direction * i) + (angled * j)) * definition.distance;
                var instanceTransform = transform(translation);
                // Now that we have the transform we transform the tool box and see if it is within the face
                // but not too close to the edges of the face
                // Note: this isn't exact, it uses the center of the face and the size of the face box 
                // rather than transforming the geometry every time.
                var xformed = box3d(toolBox.minCorner + translation, toolBox.maxCorner + translation);
                if (clash(xformed, faceBox))
                {
                    var minDistance = evDistance(context, {
                            "side0" : targetEdges,
                            "side1" : (xformed.minCorner + xformed.maxCorner) * 0.5
                    });
                    // Note: if border is zero this still results in a border that is half the size of the tool,
                    // the border is additional on top of that
                    if (minDistance.distance > (toolSize + definition.border))
                    {
                        transforms = append(transforms, instanceTransform);
                        instanceNames = append(instanceNames, "" ~ i ~ "." ~ j);
                    }
                }                
            }
        }

        // Now we set the data as needed by the applyPattern function before calling it.
        definition.patternType = PatternType.FACE;
        definition.transforms = transforms;
        definition.instanceNames = instanceNames;
        definition.seed = definition.entities;

        var remainingTransform = getRemainderPatternTransform(context, { "references" : definition.entities });
        applyPattern(context, id, definition, remainingTransform);
    }, { });

/**
 * Utility function to do a quick clash of boxes
 */
function clash(box1 is Box3d, box2 is Box3d) returns boolean
{
    for (var index = 0; index < 3; index += 1)
    {
        var min1 = box1.minCorner[index];
        var max1 = box1.maxCorner[index];
        var min2 = box2.minCorner[index];
        var max2 = box2.maxCorner[index];
        // Comparisons to zero are never a good idea because values that are calculated separately are
        // rarely the same to machine precision. FeatureScript provides tolerant equality methods
        // but not tolerant inequalities. No matter, we can fashion one ourselves by checking the
        // inequality and excluding approximate equality
        if ((max1 < min2 && !tolerantEquals(max1, min2)) || (max2 < min1 && !tolerantEquals(max2, min1)))
        {
            return false;
        }
    }
    return true;
}

Rib FeatureScript 程式碼:

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

const RIB_THICKEN_BOUNDS =
{
            "min" : -TOLERANCE.zeroLength * meter,
            "max" : 500 * meter,
            (meter) : [0.0, 0.005, 500],
            (centimeter) : 0.5,
            (millimeter) : 5.0,
            (inch) : 0.25,
            (foot) : 0.025,
            (yard) : 0.01
        } as LengthBoundSpec;

/**
 * Specifies the direction of the rib extrusion starting from the profile
 * going up to the part.
 *
 * @value NORMAL_TO_SKETCH_PLANE : The direction of the rib extrusion goes normal to the profile sketch plane.
 * @value PARALLEL_TO_SKETCH_PLANE : The direction of the rib extrusion goes parallel to the profile sketch plane.
 */
export enum RibExtrusionDirection
{
    annotation { "Name" : "Normal to sketch plane" }
    NORMAL_TO_SKETCH_PLANE,
    annotation { "Name" : "Parallel to sketch plane" }
    PARALLEL_TO_SKETCH_PLANE
}

function isClosed(context is Context, edge is Query) returns boolean
{
    return size(evaluateQuery(context, qVertexAdjacent(edge, EntityType.VERTEX))) < 2;
}

annotation { "Feature Type Name" : "Rib" }
export const rib = defineFeature(function(context is Context, id is Id, definition is map)
    precondition
    {
        annotation { "Name" : "Sketch profiles", "Filter" : EntityType.EDGE && SketchObject.YES && ConstructionObject.NO }
        definition.profiles is Query;

        annotation { "Name" : "Parts", "Filter" : EntityType.BODY }
        definition.parts is Query;

        annotation { "Name" : "Thickness" }
        isLength(definition.thickness, RIB_THICKEN_BOUNDS);

        annotation { "Name" : "Rib extrusion direction" }
        definition.ribExtrusionDirection is RibExtrusionDirection;

        annotation { "Name" : "Opposite direction", "UIHint" : "OPPOSITE_DIRECTION", "Default" : true }
        definition.oppositeDirection is boolean;

        annotation { "Name" : "Extend profiles up to part" }
        definition.extendProfilesUpToPart is boolean;

        annotation { "Name" : "Merge ribs", "Default" : true }
        definition.mergeRibs is boolean;
    }
    {
        const profiles = evaluateQuery(context, definition.profiles);
        const numberOfRibs = size(profiles);
        if (profiles == [])
        {
            throw regenError("Select sketch profiles for the rib contours");
        }

        if (evaluateQuery(context, definition.parts) == [])
        {
            throw regenError("Select parts where the rib will be fitted into");
        }

        // Create a transform for making the feature patternable via feature pattern.
        var remainingTransform = getRemainderPatternTransform(context,
                {"references" : qUnion([definition.profiles, definition.parts])});

        // Before evaluating the profiles to create the ribs, we find out how big the parts are
        // so if any extending is necessary for any rib end, we know how far we need to extend.
        // To ensure the extended profile will always go past the part(s), we use the
        // diagonal of the bounding box of the part(s) and profile(s) as the extend length.
        const partBoundingBox = evBox3d(context, {
                    "topology" : qUnion([definition.parts, definition.profiles])
                });
        const extendLength = norm(partBoundingBox.maxCorner - partBoundingBox.minCorner);

        // Create each rib (one rib per profile) as its own body.

        for (var i = 0; i < numberOfRibs; i += 1)
        {
            const profile = profiles[i];
            const thickenId = id + (i ~ "thickenRib");

            try
            {
                // Keep track of the entities we will extrude as a surface which will later
                // be thickened to create the rib.  The profile and any
                // profile extensions will need to be included in the extrude operation.
                var entitiesToExtrude = [profile];

                // Get the endpoints of the profile and the normal direction at those endpoints
                // so we can determine what needs to be extended and what direction to extend.
                const profileEndTangentLines = evEdgeTangentLines(context, {
                            "edge" : profile,
                            "parameters" : [0, 1],
                            "arcLengthParameterization" : false
                        });

                // There  are 2 reasons we might need to extend the given profiles:
                // 1.  If the profile touches the part(s), make an extension of the profile past the part to ensure
                //     that there are no gaps when we thicken the profile (this can happen if the profile is not normal
                //     to the part where they intersect).
                // 2.  The extend profiles up to part checkbox has been selected.
                const partsContainPoint = function(point is Vector) returns boolean
                    {
                        return evaluateQuery(context, qContainsPoint(definition.parts, remainingTransform * point)) != [];
                    };

                var extendProfiles = makeArray(2);
                var extendedEndPoints = makeArray(2);
                const extendDirections = [-profileEndTangentLines[0].direction, profileEndTangentLines[1].direction];

                // If the profile is closed, then there is nothing to extend.
                const isProfileClosed = isClosed(context, profile);

                for (var end in [0, 1]) // Potentially extend both endpoints of the profile curve
                {
                    extendProfiles[end] = !isProfileClosed && definition.extendProfilesUpToPart || partsContainPoint(profileEndTangentLines[end].origin);
                    if (extendProfiles[end])
                    {
                        extendedEndPoints[end] = profileEndTangentLines[end].origin + (extendDirections[end] * extendLength);
                        // This is actually a quick way to create a line in 3D
                        opFitSpline(context, id + (i ~ "extendProfile" ~ end), {
                                    "points" : [
                                            profileEndTangentLines[end].origin,
                                            extendedEndPoints[end]
                                        ]
                                });
                        entitiesToExtrude = append(entitiesToExtrude, qCreatedBy(id + (i ~ "extendProfile" ~ end), EntityType.EDGE));
                    }
                }

                // Find the direction to extrude a surface that will later be thickened to produce the rib.
                // First determine the normal or parallel direction, then, if specified,
                // choose the opposite of the normal or parallel direction.
                const profilePlane = evOwnerSketchPlane(context, { "entity" : profile });
                var ribDirection;
                if (definition.ribExtrusionDirection == RibExtrusionDirection.PARALLEL_TO_SKETCH_PLANE)
                {
                    // To get the parallel direction with the sketch plane, find the direction perpendicular
                    // to the sketch plane normal and the line that connects the start and end point of the profile.
                    const profileDirection = normalize(profileEndTangentLines[1].origin - profileEndTangentLines[0].origin);
                    ribDirection = cross(profilePlane.normal, profileDirection);
                }
                else
                {
                    ribDirection = profilePlane.normal;
                }

                if (definition.oppositeDirection)
                {
                    ribDirection = ribDirection * -1;
                }

                // Extrude a surface from the extended profile into the part(s), using the extend length
                // as the extrude depth to make sure the surface goes through the part(s).
                opExtrude(context, id + (i ~ "surfaceExtrude"), {
                            "entities" : qUnion(entitiesToExtrude),
                            "direction" : ribDirection,
                            "endDepth" : extendLength,
                            "endBound" : BoundingType.BLIND
                        });

                // Transform the extruded surface if needed to support feature pattern.
                transformResultIfNecessary(context, id + (i ~ "surfaceExtrude"), remainingTransform);

                // Thicken the surface to make the rib plus some excess material around the part(s).
                const halfThickness = definition.thickness / 2;
                opThicken(context, thickenId, {
                            "entities" : qCreatedBy(id + (i ~ "surfaceExtrude"), EntityType.FACE),
                            "thickness1" : halfThickness,
                            "thickness2" : halfThickness
                        });

                // Split the rib with the part(s) to separate the rib body from the thicken excess.
                var ribPartsQuery = qCreatedBy(thickenId, EntityType.BODY);
                opBoolean(context, id + (i ~ "splitOffRibExcess"), {
                            "tools" : definition.parts,
                            "targets" : ribPartsQuery,
                            "operationType" : BooleanOperationType.SUBTRACTION,
                            "keepTools" : true
                        });

                // Apply the remaining transform to the profile before doing collision testing.
                patternTransform(context, id + (i ~ "tr1"), profile, remainingTransform);
                // Do collision testing to help determine which parts of the thicken are excess.
                var clashes = evCollision(context, {
                        "tools" : ribPartsQuery,
                        "targets" : profile
                    });

                // Since we don't want the profile to actually move
                // move it back to it's original location after checking for collisions.
                patternTransform(context, id + (i ~ "tr2"), profile, inverse(remainingTransform));
                var clashBodies = mapArray(clashes, function(clash)
                {
                    return clash.toolBody;
                });

                // Specify a point at the end of the surface extrude.
                // Any thicken body that intersects with this point is excess.
                const surfaceExtrudeEndPoint = profileEndTangentLines[0].origin + (extendLength * ribDirection);

                // Collect up all the thicken excess and any other entities we've created leading
                // up to the thicken operation, because all of these need to be deleted.
                var entitiesToDelete = [
                    // Remove rib thicken excess sections that don't intersect the original profile.
                    qSubtraction(ribPartsQuery, qUnion(clashBodies)),

                    // Remove rib thicken excess sections that extend all the way to the end of
                    // the surface extrude (which we deliberately had extend well past the part,
                    // i.e. well past where a rib should be created).
                    qContainsPoint(ribPartsQuery, remainingTransform * surfaceExtrudeEndPoint),

                    // Remove the surface extrude, now that the thicken is completed and we don't need it anymore.
                    qCreatedBy(id + (i ~ "surfaceExtrude"), EntityType.BODY)
                ];

                // Delete any profile extensions created now that we don't need them anymore.
                // Also, any thicken section that intersects with the far end of an extension
                // (i.e. not the end that intersects with the profile) is thicken excess and should be deleted.
                for (var end in [0, 1])
                {
                    if (extendProfiles[end])
                    {
                        entitiesToDelete = append(entitiesToDelete, qCreatedBy(id + (i ~ "extendProfile" ~ end), EntityType.BODY));
                        entitiesToDelete = append(entitiesToDelete, qContainsPoint(ribPartsQuery, extendedEndPoints[end]));
                    }
                }

                opDeleteBodies(context, id + (i ~ "deleteRibExcess"), {
                            "entities" : qUnion(entitiesToDelete)
                        });
            }
            catch
            {
                throw regenError('Failed to create a rib from a selected profile.',
                        profile);
            }

            // Fail early if the rib body can't be created.
            if (evaluateQuery(context, qCreatedBy(thickenId, EntityType.BODY)) == [])
            {
                throw regenError('Selected profile did not produce a rib body.  Make sure the rib direction and alignment are correct.',
                        profile);
            }
        }

        // Optionally, merge the new ribs with the original parts.
        if (definition.mergeRibs)
        {
            // The original parts are first in the tools query so that they
            // will maintain their names.
            var toMerge = [definition.parts];
            for (var i = 0; i < numberOfRibs; i += 1)
            {
                toMerge = append(toMerge, qCreatedBy(id, EntityType.BODY));
            }

            try
            {
                opBoolean(context, id + "mergeRibsWithParts", {
                            "tools" : qUnion(toMerge),
                            "operationType" : BooleanOperationType.UNION
                        });
            }
            catch
            {
                throw regenError('Failed to merge ribs into parts.');
            }
        }
    },
        {
            oppositeDirection : true,
            ribExtrusionDirection : RibExtrusionDirection.NORMAL_TO_SKETCH_PLANE,
            extendProfilesUpToPart : false,
            mergeRibs : true
        });

function patternTransform(context, id, query, transform)
{
    if (transform == identityTransform())
        return;
    opTransform(context, id, {
            "bodies" : qOwnerBody(query),
            "transform" : transform
    });
}