Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape.

First time visiting? Here are some places to start:

  1. Looking for a certain topic? Check out the categories filter or use Search (upper right).
  2. Need support? Ask a question to our Community Support category.
  3. Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
  4. Be respectful, on topic and if you see a problem, Flag it.

If you would like to contact our Community Manager personally, feel free to send a private message or an email.

Filling functionality gaps with FeatureScript -- Rib Feature, 3D Spline, fasteners, and more

ilya_baranilya_baran Onshape Employees, Developers, HDM Posts: 915
edited March 2016 in FeatureScript
Onshape does not yet have every feature that we believe it should.  While building features to our high standard of usability takes time, one of the uses of FeatureScript is providing immediate workarounds that get the job done, but may be less polished than what we'd consider shipping.

I'm collecting a few such workarounds here by posting the source code, which you can copy/paste into a Feature Studio and then either use as-is or customize.

I'll start with a feature that makes a 3D spline through a sequence of vertex selections and a feature that makes a surface from an uploaded csv file representing height data.  Kori added a very nice rib feature, Cody added a point-driven body pattern, and Neil added some fastener features and a wave spring.
Ilya Baran \ Director of FeatureScript \ Onshape Inc

Comments

  • ilya_baranilya_baran Onshape Employees, Developers, HDM Posts: 915
    edited January 2016
    3D Spline:

    Code:
    annotation { "Feature Type Name" : "3D Spline" }
    export const spline3D = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Vertices", "Filter" : EntityType.VERTEX }
            definition.vertices is Query;
            
            annotation { "Name" : "Closed" }
            definition.closed is boolean;        
        }
        {
            var points = [];
            for (var vertex in evaluateQuery(context, definition.vertices))
            {
                points = append(points, evVertexPoint(context, { "vertex" : vertex }));
            }
            if (definition.closed)
            {
                if (size(points) <= 2)
                    throw regenError(ErrorStringEnum.INVALID_INPUT);
                points = append(points, points[0]);
            }
            opFitSpline(context, id, { "points" : points });
        }, { closed : false });
    
    Ilya Baran \ Director of FeatureScript \ Onshape Inc
  • ilya_baranilya_baran Onshape Employees, Developers, HDM Posts: 915
    edited January 2016
    CSV Height surface:

    Code:
    // Upload a CSV file with a rectangular array of heights and put the tab id of the upload into the path
    // of this import.  One day it'll be possible to pass the tab as a parameter to this feature instead.
    import(path : "", version : "");
    
    annotation { "Feature Type Name" : "CSV-driven surface" }
    export const csvSurface = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {        
            annotation { "Name" : "Width scale" }
            isLength(definition.widthScale, LENGTH_BOUNDS);
    
            annotation { "Name" : "Height scale" }
            isLength(definition.heightScale, LENGTH_BOUNDS);
        }
        {
            const data = BLOB_DATA.csvData;
    
            var outerIdx = 0;
            var profiles = [];
            for (var row in data) // Create a spline for each row
            {
                var innerIdx = 0;
                var points = [];
                for (var height in row)
                {
                    points = append(points, vector(outerIdx * definition.widthScale,
                                innerIdx * definition.widthScale,
                                height * definition.heightScale));
                    innerIdx += 1;
                }
                const splineId = id + ("spline" ~ outerIdx);
                opFitSpline(context, splineId, { "points" : points });
    
                profiles = append(profiles, qCreatedBy(splineId, EntityType.EDGE));
                outerIdx += 1;
            }
    
            // Now loft between the splines
            opLoft(context, id + "loft", { profileSubqueries : profiles, bodyType : ToolBodyType.SURFACE });
            // And delete the splines
            opDeleteBodies(context, id + "delete", { "entities" : qUnion(profiles) });
        });
    
    Ilya Baran \ Director of FeatureScript \ Onshape Inc
  • korikori Onshape Employees Posts: 15
    edited May 2016
    Rib feature:

    Code:
    const 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;
    annotation { "Feature Type Name" : "Rib" } export const rib = defineFeature(function(context is Context, id is Id, definition is map) precondition { annotation { "Name" : "Profiles", "Filter" : EntityType.EDGE } definition.profiles is Query; annotation { "Name" : "Parts", "Filter" : EntityType.BODY } definition.parts is Query; annotation { "Name" : "Thickness" } isLength(definition.thickness, THICKEN_BOUNDS); annotation { "Name" : "Opposite direction", "UIHint" : "OPPOSITE_DIRECTION", "Default" : true } definition.oppositeDirection is boolean; annotation { "Name" : "Parallel to sketch plane" } definition.ribDirectionIsParallelToSketchPlane is boolean; annotation { "Name" : "Extend profiles up to part" } definition.extendProfilesUpToPart is boolean; annotation { "Name" : "Merge ribs", "Default" : true } definition.mergeRibs is boolean; } { // 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. const profiles = evaluateQuery(context, definition.profiles); const numberOfRibs = size(profiles); for (var i = 0; i < numberOfRibs; i += 1) { const profile = profiles[i]; // 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, point)) != []; }; var extendProfiles = makeArray(2); var extendedEndPoints = makeArray(2); const extendDirections = [-profileEndTangentLines[0].direction, profileEndTangentLines[1].direction]; for (var end in [0, 1]) // Potentially extend both endpoints of the profile curve { extendProfiles[end] = 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.ribDirectionIsParallelToSketchPlane) { // 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 }); // Thicken the surface to make the rib plus some excess material around the part(s). const halfThickness = definition.thickness / 2; const thickenId = id + (i ~ "thickenRib"); 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 }); // Do collision testing to help determine which parts of the thicken are excess. var clashes = evCollision(context, { "tools" : ribPartsQuery, "targets" : profile }); 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, 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) }); } // Optionally, merge the new ribs with the original part(s). 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 + (i ~ "thickenRib"), EntityType.BODY)); } opBoolean(context, id + "mergeRibsWithParts", { "tools" : qUnion(toMerge), "operationType" : BooleanOperationType.UNION }); } }, { oppositeDirection : true, ribDirectionIsParallelToSketchPlane : false, extendProfilesUpToPart : false, mergeRibs : false });
  • cody_armstrongcody_armstrong Moderator, Onshape Employees, Developers Posts: 184
    edited January 2016
    Point Driven Pattern: 

    Code:
    import(path : "onshape/std/geometry.fs", version : "");
    import(path : "onshape/std/patternUtils.fs", version : ""); export import(path : "onshape/std/tool.fs", version : "");
    annotation { "Feature Type Name" : "Point Driven Pattern" } export const myFeature = defineFeature(function(context is Context, id is Id, definition is map) precondition { booleanStepTypePredicate(definition); annotation { "Name" : "Entities to pattern", "Filter" : EntityType.BODY } definition.entities is Query; annotation { "Name" : "Reference Point", "Filter" : EntityType.VERTEX, "MaxNumberOfPicks" : 1 } definition.reference is Query; annotation { "Name" : "Locations", "Filter" : EntityType.VERTEX && SketchObject.YES } definition.locations is Query; booleanStepScopePredicate(definition); } { // Determine where to pattern const locations = evaluateQuery(context, definition.locations); verifyPatternSize(context, id, size(locations)); // Compute the origin var origin; if (evaluateQuery(context, definition.reference) == []) { const boxResult = evBox3d(context, { topology : definition.entities }); origin = (boxResult.minCorner + boxResult.maxCorner) / 2; } else { origin = evVertexPoint(context, { "vertex" : definition.reference }); } // Compute the transforms and instance names var instanceNames = []; var transforms = []; var patternNumber = 1; for (var location in locations) { var point is Vector = evVertexPoint(context, { "vertex" : location }); transforms = append(transforms, transform(point - origin)); instanceNames = append(instanceNames, "" ~ patternNumber); patternNumber += 1; } definition.transforms = transforms; definition.instanceNames = instanceNames; // Actually pattern opPattern(context, id, definition); processPatternBooleansIfNeeded(context, id, definition); }, { isFacePattern : false /* So that patternUtils functions work */, operationType : NewBodyOperationType.NEW });
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 2,088
    edited March 2016
    Wave Spring example:



    annotation { "Feature Type Name" : "Wave Spring" }
    export const WaveSpring = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Outside Diameter", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.outerDiameter, LENGTH_BOUNDS);
    
            annotation { "Name" : "Inside Diameter", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.innerDiameter, LENGTH_BOUNDS);
    
            annotation { "Name" : "Free height", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.freeHeight, LENGTH_BOUNDS);
    
            annotation { "Name" : "Number of turns", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isInteger(definition.turns, POSITIVE_COUNT_BOUNDS);
    
            annotation { "Name" : "Waves per turn", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isInteger(definition.waves, POSITIVE_COUNT_BOUNDS);
    
            annotation { "Name" : "Wire thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.thickness, LENGTH_BOUNDS);
        }
        {
            var outerPoints = [];
            var innerPoints = [];
            const outerRadius = definition.outerDiameter / 2;
            const innerRadius = definition.innerDiameter / 2;
            const waves = definition.waves + 0.5; // so waves are not the same with each turn
            var turns = definition.turns;
    
            if (turns % 2 == 0) // if it's an even number of turns
                turns = turns - (0.5 / waves); //shorten by half a wave
    
            // The height of the spring is approximately given by:
            // H = freeHeight * (maxT - minT) + 2 * waveHeight + thickness
            // waveHeight = (freeHeight / turns - thickness) / 2 - 0.05 * thickness
            // maxT ~= 1 - 1 / (turns * waves * 2)
            // minT ~= 1 / (turns * waves * 2)
            // So: H = freeHeight * (1 - 1 / (turns * waves)) + (freeHeight / turns - thickness) - 0.1 * thickness + thickness
            // So: H = freeHeight * (1 - 1 / (turns * waves) + 1 / turns) - 0.1 * thickness
            const clearance = 0.02; // arbitrary small number to prevent self intersecting bodies
            const freeHeight = (definition.freeHeight + clearance * 2 * definition.thickness) / (1 - 1 / (waves * turns) + 1 / turns);
            const waveHeight = (freeHeight / turns - definition.thickness) / 2 - clearance * definition.thickness; 
    
            for (var t = 0; t <= 1 + 1e-8; t += (1 / (60 * turns)))
            {
                var angle = 2 * PI * t * radian * turns;
                outerPoints = append(outerPoints, vector(outerRadius * cos(angle), outerRadius * sin(angle), waveHeight * cos(waves * angle) + freeHeight * t));
                innerPoints = append(innerPoints, vector(innerRadius * cos(angle), innerRadius * sin(angle), waveHeight * cos(waves * angle) + freeHeight * t));
            }
    
            opFitSpline(context, id + "fitSpline1", {
                        "points" : outerPoints
                    });
            opFitSpline(context, id + "fitSpline2", {
                        "points" : innerPoints
                    });
            opLoft(context, id + "loft1", {
                        "profileSubqueries" : [qCreatedBy(id + "fitSpline1", EntityType.EDGE), qCreatedBy(id + "fitSpline2", EntityType.EDGE)],
                        "bodyType" : ToolBodyType.SURFACE
                    });
            opThicken(context, id + "thicken1", {
                        "entities" : qCreatedBy(id + "loft1", EntityType.FACE),
                        "thickness1" : 0 * meter,
                        "thickness2" : definition.thickness
                    });
    
            // Make bottom of spring coincident with top plane
            const springBox = evBox3d(context, { "topology" : qCreatedBy(id + "thicken1", EntityType.BODY) });
            const transformVector = vector(0 * meter, 0 * meter, -springBox.minCorner[2]);
            const transformMatrix = transform(transformVector);
    
            opTransform(context, id + "transform1", {
                        "bodies" : qCreatedBy(id + "thicken1", EntityType.BODY),
                        "transform" : transformMatrix
                    });
    
            // Add mate connectors top and bottom for easy assembly
            opMateConnector(context, id + "mateConnector1", {
                        "coordSystem" : coordSystem(vector(0, 0, 0), vector(1, 0, 0), vector(0, 0, -1)),
                        "owner" : qCreatedBy(id + "thicken1", EntityType.BODY)
                    });
            opMateConnector(context, id + "mateConnector2", {
                        "coordSystem" : coordSystem(vector(0, 0, springBox.maxCorner[2] - springBox.minCorner[2]), vector(1, 0, 0), vector(0, 0, 1)),
                        "owner" : qCreatedBy(id + "thicken1", EntityType.BODY)
                    });
    
            // Delete splines and surface
            var bodies = [qCreatedBy(id + "fitSpline1"), qCreatedBy(id + "fitSpline2"), qCreatedBy(id + "loft1")];
            opDeleteBodies(context, id + "delete", { "entities" : qUnion(bodies) });
    
        }, { /* default parameters */ });
    


    ws.png 302.5K
    Neil Cooke, Director of Technical Marketing, Onshape Inc.
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 2,088
    edited March 2016
    Snap fit feature. This feature is incomplete, but will satisfy most scenarios. I will update this code when I get around to finishing it.... :smiley:   Use the values shown in the image to get started. Prerequisite: sketch with points.



    export enum HookStyle
    {
        annotation { "Name" : "Blind" }
        BLIND,
        annotation { "Name" : "Up to face" }
        PLANE
    }
    
    annotation { "Feature Type Name" : "Snap Hook" }
    export const SnapHook = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        { 
            annotation { "Name" : "Sketch points to place snap hooks", "Filter" : EntityType.VERTEX && SketchObject.YES && ConstructionObject.NO }
            definition.locations is Query;
    
            annotation { "Name" : "Hook style" }
            definition.style is HookStyle;
    
            if (definition.style == HookStyle.BLIND)
            {
                annotation { "Name" : "Hook height", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
                isLength(definition.height, LENGTH_BOUNDS);
            }
            else
            {
                annotation { "Name" : "Parallel face or plane", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
                definition.parallelFace is Query;
            }
    
            annotation { "Name" : "Hook width", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.hookWidth, LENGTH_BOUNDS);
    
            annotation { "Name" : "Flip hook direction", "UIHint" : "OPPOSITE_DIRECTION" }
            definition.hookFlipDirection is boolean;
    
            annotation { "Name" : "Edge to define hook direction", "Filter" : EntityType.EDGE, "MaxNumberOfPicks" : 1 }
            definition.hookDirection is Query;
    
            annotation { "Name" : "Deflection distance", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.hookDepth, LENGTH_BOUNDS);
    
            annotation { "Name" : "Flat height/width", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.flatHeight, NONNEGATIVE_LENGTH_BOUNDS);
    
            annotation { "Name" : "Hook thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.hookThickness, NONNEGATIVE_LENGTH_BOUNDS);
    
            annotation { "Name" : "Deflection angle", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isAngle(definition.deflectionAngle, ANGLE_STRICT_90_BOUNDS);
    
            annotation { "Name" : "Return angle", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isAngle(definition.returnAngle, ANGLE_STRICT_90_BOUNDS);
    
            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" : "Undercut", "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;
    
            var perpHookVector = vector(hookVector[1] * -1, hookVector[0]);
    
            // define the plane for the underside of the snaphook
            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;
    
            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);
    
            definition.elemAdd = true;
            definition.elemId = 1;
            definition.entities = qSketchRegion(id + "sketch2");
            definition.boundingType = BoundingType.UP_TO_BODY;
            definition.draftPullDirection = false;
            definition.oppositeDirection = true;
    
            buildBoss(context, id, definition);
    
            definition.elemAdd = true;
            definition.elemId = 2;
            definition.entities = qSketchRegion(id + "sketch3");
            definition.boundingType = BoundingType.BLIND;
            definition.draftPullDirection = true;
            definition.oppositeDirection = false;
    
            buildBoss(context, id, definition);
    
            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)
            {
                definition.elemAdd = false;
                definition.elemId = 3;
                definition.entities = qSketchRegion(id + "sketch1");
                definition.boundingType = BoundingType.THROUGH_ALL;
                definition.draftPullDirection = false;
                definition.oppositeDirection = true;
    
                buildBoss(context, id, definition);
            }
    
            // Remove sketch entities - no longer required
            var sketches = [qCreatedBy(id + "sketch1"), qCreatedBy(id + "sketch2"), qCreatedBy(id + "sketch3"), qCreatedBy(id + "plane1")];
            opDeleteBodies(context, id + "delete", { "entities" : qUnion(sketches) });
        },
            {
            /* default parameters */
            });
    
    // buildBoss function is called for each element of the feature - extrude then boolean
    function buildBoss(context is Context, id is Id, definition is map)
    {
        extrude(context, id + ("extrude" ~ definition.elemId), {
                    "entities" : definition.entities,
                    "endBound" : definition.boundingType,
                    "depth" : definition.depth,
                    "endBoundEntityBody" : definition.targetBody,
                    "oppositeDirection" : definition.oppositeDirection,
                    "hasDraft" : definition.hasDraft,
                    "draftAngle" : definition.draftAngle,
                    "draftPullDirection" : definition.draftPullDirection
                });
    
        if (definition.elemAdd)
        {
            opBoolean(context, id + ("boolean" ~ definition.elemId), {
                        "tools" : qUnion([definition.targetBody, qBodyType(qCreatedBy(id + ("extrude" ~ definition.elemId), EntityType.BODY), BodyType.SOLID)]),
                        "operationType" : BooleanOperationType.UNION
                    });
        }
        else
        {
            opBoolean(context, id + ("boolean" ~ definition.elemId), {
                        "tools" : qBodyType(qCreatedBy(id + ("extrude" ~ definition.elemId), EntityType.BODY), BodyType.SOLID),
                        "targets" : definition.targetBody,
                        "operationType" : BooleanOperationType.SUBTRACTION
                    });
        }
    }
    

    sf.png 215.9K
    Neil Cooke, Director of Technical Marketing, Onshape Inc.
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 2,088
    Mounting boss feature. This feature covers only one type of boss (screw boss), but with many editable variables. Could easily be converted to include all other types. Use the values shown in the image to get started. Prerequisite: sketch with points.



    export const RIB_COUNT_BOUNDS =
    {
                "min" : 1,
                "max" : 6,
                (unitless) : [1, 4, 6]
            } as IntegerBoundSpec;
    
    export enum BossStyle
    {
        annotation { "Name" : "Blind" }
        BLIND,
        annotation { "Name" : "Up to face" }
        PLANE
    }
    
    annotation { "Feature Type Name" : "Mounting Boss" }
    export const myFeature = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        { 
            annotation { "Name" : "Sketch points to place bosses", "Filter" : EntityType.VERTEX && SketchObject.YES && ConstructionObject.NO }
            definition.locations is Query;
    
            annotation { "Name" : "Boss style" }
            definition.style is BossStyle;
    
            if (definition.style == BossStyle.BLIND)
            {
                annotation { "Name" : "Boss height", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
                isLength(definition.height, LENGTH_BOUNDS);
            }
            else
            {
                annotation { "Name" : "Parallel face or plane", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 }
                definition.parallelFace is Query;
            }
    
            annotation { "Name" : "Boss diameter", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.diameter, LENGTH_BOUNDS);
    
            annotation { "Name" : "Ribs", "Default" : true, "UIHint" : "DISPLAY_SHORT" }
            definition.hasRibs is boolean;
    
            if (definition.hasRibs == true)
            {
                annotation { "Name" : "Number of ribs (max 6)", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"] }
                isInteger(definition.ribCount, RIB_COUNT_BOUNDS);
    
                annotation { "Name" : "Flip rib direction", "UIHint" : "OPPOSITE_DIRECTION" }
                definition.ribFlipDirection is boolean;
    
                annotation { "Name" : "Edge to define rib direction", "Filter" : EntityType.EDGE, "MaxNumberOfPicks" : 1 }
                definition.ribDirection is Query;
    
                annotation { "Name" : "Rib diameter at top", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
                isLength(definition.ribDiameter, NONNEGATIVE_LENGTH_BOUNDS);
    
                annotation { "Name" : "Rib distance from top", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
                isLength(definition.ribHeight, NONNEGATIVE_LENGTH_BOUNDS);
    
                annotation { "Name" : "Rib thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
                isLength(definition.ribThickness, THICKEN_BOUNDS);
    
                annotation { "Name" : "Chamfer", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"], "Default" : true }
                definition.hasChamfer is boolean;
    
                if (definition.hasChamfer == true)
                {
                    annotation { "Name" : "Chamfer size", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"] }
                    isLength(definition.chamferSize, NONNEGATIVE_LENGTH_BOUNDS);
                }
            }
    
            annotation { "Name" : "Hole diameter", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.holeDiam, NONNEGATIVE_LENGTH_BOUNDS);
    
            annotation { "Name" : "Wall thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" }
            isLength(definition.wallThickness, NONNEGATIVE_LENGTH_BOUNDS);
    
            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" : "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 ribPlane;
    
            // define the plane for the top of the boss
            if (definition.style == BossStyle.PLANE && definition.parallelFace != undefined)
                topPlane = evPlane(context, { "face" : definition.parallelFace });
            else
                topPlane = plane(sketchPlane.origin + definition.height * sketchPlane.normal, sketchPlane.normal);
    
            definition.elemAdd = true;
            definition.elemId = 1;
            var nameId = 1;
    
            definition.sketch = newSketchOnPlane(context, id + "sketch1", { "sketchPlane" : topPlane });
    
            // Build first feature - extruded circle
            for (var location in locations)
            {
                var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location }));
    
                skCircle(definition.sketch, "circle" ~ nameId, {
                            "center" : vector(point[0], point[1]),
                            "radius" : definition.diameter / 2
                        });
                nameId += 1;
            }
    
            buildBoss(context, id, definition);
    
            // Build second feature - extruded ribs
            if (definition.hasRibs)
            {
                // define top of ribs
                ribPlane = plane(topPlane.origin - definition.ribHeight * topPlane.normal, topPlane.normal);
    
                var ribVector = vector(0, 1); // by default pointing up in Y
    
                // if user has defined rib direction, work out the vector
                if (definition.ribDirection != undefined)
                {
                    const directionResult = try(evAxis(context, { "axis" : definition.ribDirection }));
    
                    if (directionResult != undefined)
                        ribVector = normalize(vector(directionResult.direction[0], directionResult.direction[1]));
                }
    
                if (definition.ribFlipDirection)
                    ribVector = ribVector * -1;
    
                definition.sketch = newSketchOnPlane(context, id + "sketch2", { "sketchPlane" : ribPlane });
    
                const ribPlaneCSys = planeToCSys(ribPlane);
                var chamferPoints = [];
                definition.elemId = 2;
                nameId = 1;
    
                for (var location in locations)
                {
                    var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location }));
    
                    const center = vector(point[0], point[1]);
    
                    // Build an closed "star" shaped sketch to represent the ribs
                    for (var j = 0; j < definition.ribCount; j += 1)
                    {
                        var angle = (360 / definition.ribCount) * j * degree;
    
                        // The angle for each rib
                        var angledRibVector = vector(ribVector[0] * cos(angle) - ribVector[1] * sin(angle),
                            ribVector[0] * sin(angle) + ribVector[1] * cos(angle));
    
                        var perpRibVector = vector(angledRibVector[1] * -1, angledRibVector[0]);
    
                        var ribOffset = definition.ribThickness / 2 / tan(180 / definition.ribCount * degree);
    
                        if (definition.ribCount == 1)
                            ribOffset = 0 * meter;
    
                        var points = [
                            center - (definition.ribThickness / 2) * perpRibVector + (ribOffset) * angledRibVector,
                            center - (definition.ribThickness / 2) * perpRibVector + (definition.ribDiameter / 2) * angledRibVector,
                            center + (definition.ribThickness / 2) * perpRibVector + (definition.ribDiameter / 2) * angledRibVector,
                            center + (definition.ribThickness / 2) * perpRibVector + (ribOffset) * angledRibVector];
    
                        for (var i = 0; i < size(points); i += 1)
                        {
                            skLineSegment(definition.sketch, "line" ~ nameId,
                                    { "start" : points[i],
                                        "end" : points[(i + 1) % size(points)]
                                    });
                            nameId += 1;
                        }
    
                        // Keep a list of the centerpoints of the edges where the chamfers may go
                        var chamferPoint2d = center + (definition.ribDiameter / 2) * angledRibVector;
                        chamferPoints = append(chamferPoints, toWorld(ribPlaneCSys, vector(chamferPoint2d[0], chamferPoint2d[1], 0 * meter)));
                    }
                    nameId += 1;
                }
    
                buildBoss(context, id, definition);
    
                // Build third feature - chamfers
                if (definition.hasChamfer)
                {
                    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.EQUAL_OFFSETS,
                                    "width" : definition.chamferSize
                                }));
                }
            }
    
            definition.elemAdd = false;
            definition.elemId = 3;
            nameId = 1;
    
            // Build fourth feature - through hole to outside of part
            var holePlane = plane(topPlane.origin - definition.wallThickness * topPlane.normal, topPlane.normal);
    
            definition.sketch = newSketchOnPlane(context, id + "sketch3", { "sketchPlane" : holePlane });
    
            for (var location in locations)
            {
                var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location }));
    
                skCircle(definition.sketch, "circle" ~ nameId, {
                            "center" : vector(point[0], point[1]),
                            "radius" : definition.diameter / 2 - definition.wallThickness
                        });
                nameId += 1;
            }
    
            buildBoss(context, id, definition);
    
            definition.elemId = 4;
            nameId = 1;
    
            // Build fifth feature - screw hole
            definition.sketch = newSketchOnPlane(context, id + "sketch4", { "sketchPlane" : topPlane });
    
            for (var location in locations)
            {
                var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location }));
    
                skCircle(definition.sketch, "circle" ~ nameId, {
                            "center" : vector(point[0], point[1]),
                            "radius" : definition.holeDiam / 2
                        });
                nameId += 1;
            }
    
            buildBoss(context, id, definition);
    
            // Remove sketch entities - no longer required
            var sketches = [qCreatedBy(id + "sketch1"), qCreatedBy(id + "sketch2"), qCreatedBy(id + "sketch3"), qCreatedBy(id + "sketch4")];
            opDeleteBodies(context, id + "delete", { "entities" : qUnion(sketches) });
        },
            {
            /* default parameters */
            });
    
    // buildBoss function is called for each element of the feature - extrude then boolean
    function buildBoss(context is Context, id is Id, definition is map)
    {
        skSolve(definition.sketch);
    
        extrude(context, id + ("extrude" ~ definition.elemId), {
                    "entities" : qSketchRegion(id + ("sketch" ~ definition.elemId)),
                    "endBound" : BoundingType.UP_TO_BODY,
                    "endBoundEntityBody" : definition.targetBody,
                    "oppositeDirection" : true,
                    "hasDraft" : definition.hasDraft,
                    "draftAngle" : definition.draftAngle,
                    "draftPullDirection" : false
                });
    
        if (definition.elemAdd)
        {
            opBoolean(context, id + ("boolean" ~ definition.elemId), {
                        "tools" : qUnion([definition.targetBody, qBodyType(qCreatedBy(id + ("extrude" ~ definition.elemId), EntityType.BODY), BodyType.SOLID)]),
                        "operationType" : BooleanOperationType.UNION
                    });
        }
        else
        {
            opBoolean(context, id + ("boolean" ~ definition.elemId), {
                        "tools" : qBodyType(qCreatedBy(id + ("extrude" ~ definition.elemId), EntityType.BODY), BodyType.SOLID),
                        "targets" : definition.targetBody,
                        "operationType" : BooleanOperationType.SUBTRACTION
                    });
        }
    }
    mb.png 369.7K
    Neil Cooke, Director of Technical Marketing, Onshape Inc.
  • NeilCookeNeilCooke Moderator, Onshape Employees Posts: 2,088
    Spur gear generator - needs a sketch vertex to position the gear center. Position multiple meshing gears together by creating your gears first (using same module/diametral pitch/circular pitch), then modify your original positioning sketch and set the distance between the gears using half the sum of the reported pitch circle diameter for each gear.



    annotation { "Feature Type Name" : "Spur Gear", "Editing Logic Function" : "editGearLogic" }
    export const SpurGear = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Number of teeth" }
            isInteger(definition.numTeeth, TEETH_BOUNDS);
    
            annotation { "Name" : "Input type" }
            definition.GearInputType is GearInputType;
    
            if (definition.GearInputType == GearInputType.MD)
            {
                annotation { "Name" : "Module" }
                isLength(definition.MD, MODULE_BOUNDS);
            }
            if (definition.GearInputType == GearInputType.DP)
            {
                annotation { "Name" : "Diametral pitch" }
                isReal(definition.DP, POSITIVE_REAL_BOUNDS);
            }
            if (definition.GearInputType == GearInputType.CP)
            {
                annotation { "Name" : "Circular pitch" }
                isLength(definition.CP, LENGTH_BOUNDS);
            }
            annotation { "Name" : "Pitch circle diameter" }
            isLength(definition.PCD, LENGTH_BOUNDS);
    
            annotation { "Name" : "Pressure angle" }
            isAngle(definition.PA, PRESSURE_ANGLE_BOUNDS);
    
            annotation { "Name" : "Center hole" }
            definition.centerHole is boolean;
    
            if (definition.centerHole)
            {
                annotation { "Name" : "Hole diameter" }
                isLength(definition.centerHoleDia, CENTERHOLE_BOUNDS);
    
                annotation { "Name" : "Keyway" }
                definition.key is boolean;
    
                if (definition.key)
                {
                    annotation { "Name" : "Key width" }
                    isLength(definition.keyWidth, KEY_WIDTH_BOUNDS);
    
                    annotation { "Name" : "Key height" }
                    isLength(definition.keyHeight, KEY_HEIGHT_BOUNDS);
    
                    annotation { "Name" : "Key angle" }
                    isAngle(definition.keyOffset, OFFSET_ANGLE_BOUNDS);
                }
            }
    
            annotation { "Name" : "Center point", "Filter" : EntityType.VERTEX && SketchObject.YES, "MaxNumberOfPicks" : 1 }
            definition.center is Query;
    
            annotation { "Name" : "Extrude depth" }
            isLength(definition.gearDepth, BLEND_BOUNDS);
    
            annotation { "Name" : "Extrude direction", "UIHint" : "OPPOSITE_DIRECTION" }
            definition.flipGear is boolean;
    
            annotation { "Name" : "Offset angle" }
            isAngle(definition.offset, OFFSET_ANGLE_BOUNDS);
        }
        {
           
            const addendum = definition.MD;
            const dedendum = 1.25 * definition.MD;
            const base = definition.PCD * cos(definition.PA);
    
            const location = evaluateQuery(context, definition.center)[0];
            const sketchPlane is Plane = evOwnerSketchPlane(context, { "entity" : location });
            const gearSketch = newSketchOnPlane(context, id + "gearSketch", { "sketchPlane" : sketchPlane });
            const center3D = evVertexPoint(context, { "vertex" : location });
            const center2D = worldToPlane(sketchPlane, center3D);
    
            skCircle(gearSketch, "addendum", {
                        "center" : center2D,
                        "radius" : definition.PCD / 2 + addendum
                    });
    
            var nameId = 1;
            var filletEdges = [];
    
            // angle between root of teeth
            var alpha = sqrt(definition.PCD ^ 2 - base ^ 2) / base * radian - definition.PA;
            var beta = 360 / (4 * definition.numTeeth) * degree - alpha;
    
    
            for (var teeth = 0; teeth < definition.numTeeth; teeth += 1)
            {
                var involute1 = [];
                var involute2 = [];
                var arcDone = false;
    
                for (var t = 0; t <= 2; t += (1 / 20))
                {
                    var angle = t * radian;
                    var offset = ((360 / definition.numTeeth) * PI * teeth) / 180 * radian + beta + definition.offset;
                    var ca = cos(angle + offset);
                    var sa = sin(angle + offset);
                    var cab = cos(offset - beta * 2 - angle);
                    var sab = sin(offset - beta * 2 - angle);
    
                    if (base >= definition.PCD - 2 * dedendum && t == 0)
                    {
                        var point1 = vector((definition.PCD / 2 - dedendum) * ca, (definition.PCD / 2 - dedendum) * sa);
                        var point2 = vector((definition.PCD / 2 - dedendum) * cab, (definition.PCD / 2 - dedendum) * sab);
                        involute1 = append(involute1, point1 + center2D);
                        involute2 = append(involute2, point2 + center2D);
    
                        var mid = getArcMidPoint(center2D, point2 + center2D, point1 + center2D);
    
                        skArc(gearSketch, "arc" ~ nameId, {
                                    "start" : point2 + center2D,
                                    "mid" : mid,
                                    "end" : point1 + center2D
                                });
    
                        filletEdges = append(filletEdges, toWorldVector(planeToCSys(sketchPlane), point2 + center2D, definition.gearDepth, definition.flipGear));
                        filletEdges = append(filletEdges, toWorldVector(planeToCSys(sketchPlane), point1 + center2D, definition.gearDepth, definition.flipGear));
    
                        arcDone = true;
                    }
    
                    var point1 = vector(base * 0.5 * (ca + t * sa), base * 0.5 * (sa - t * ca));
                    var point2 = vector(base * 0.5 * (cab - t * sab), base * 0.5 * (sab + t * cab));
                    involute1 = append(involute1, point1 + center2D);
                    involute2 = append(involute2, point2 + center2D);
    
                    if (!arcDone)
                    {
                        var mid = getArcMidPoint(center2D, point2 + center2D, point1 + center2D);
    
                        skArc(gearSketch, "arc" ~ nameId, {
                                    "start" : point2 + center2D,
                                    "mid" : mid,
                                    "end" : point1 + center2D
                                });
    
                        filletEdges = append(filletEdges, toWorldVector(planeToCSys(sketchPlane), point2 + center2D, definition.gearDepth, definition.flipGear));
                        filletEdges = append(filletEdges, toWorldVector(planeToCSys(sketchPlane), point1 + center2D, definition.gearDepth, definition.flipGear));
    
                        arcDone = true;
                    }
    
                    if (sqrt(point1[0] ^ 2 + point1[1] ^ 2) >= (definition.PCD / 2 + addendum))
                        break;
                }
    
                skFitSpline(gearSketch, "spline-a" ~ nameId, {
                            "points" : involute1
                        });
    
                skFitSpline(gearSketch, "spline-b" ~ nameId, {
                            "points" : involute2
                        });
    
                nameId += 1;
            }
    
            const skFace = qContainsPoint(qCreatedBy(id + "sketch", EntityType.FACE), center3D);
    
            if (definition.centerHole)
            {
                if (definition.key)
                {
                    var keyVector = vector(0, 1); // by default pointing up in Y
    
                    var angledKeyVector = vector(keyVector[0] * cos(definition.keyOffset) - keyVector[1] * sin(definition.keyOffset),
                        keyVector[0] * sin(definition.keyOffset) + keyVector[1] * cos(definition.keyOffset));
    
                    var perpKeyVector = vector(angledKeyVector[1] * -1, angledKeyVector[0]);
    
                    var points = [
                        center2D - (definition.keyWidth / 2) * perpKeyVector,
                        center2D - (definition.keyWidth / 2) * perpKeyVector + (definition.keyHeight - definition.centerHoleDia / 2) * angledKeyVector,
                        center2D + (definition.keyWidth / 2) * perpKeyVector + (definition.keyHeight - definition.centerHoleDia / 2) * angledKeyVector,
                        center2D + (definition.keyWidth / 2) * perpKeyVector];
    
                    for (var i = 0; i < size(points); i += 1)
                    {
                        skLineSegment(gearSketch, "line" ~ nameId,
                                { "start" : points[i],
                                    "end" : points[(i + 1) % size(points)]
                                });
                        nameId += 1;
                    }
                }
    
                skCircle(gearSketch, "Center", {
                            "center" : center2D,
                            "radius" : definition.centerHoleDia / 2
                        });
            }
            skSolve(gearSketch);
    
            var skRegions = evaluateQuery(context, qSketchRegion(id + "gearSketch"));
    
            extrude(context, id + "extrude1", {
                        "entities" : skRegions[size(skRegions) - 1], // last region in sketch
                        "endBound" : BoundingType.BLIND,
                        "depth" : definition.gearDepth,
                        "oppositeDirection" : definition.flipGear
                    });
    
            var filletEdges3D = [];
    
            for (var i = 0; i < size(filletEdges); i += 1)
            {
                // Find the edges that intersect the points previously collected
                filletEdges3D = append(filletEdges3D, qContainsPoint(qCreatedBy(id + "extrude1", EntityType.EDGE), filletEdges[i]));
            }
    
            const filletRadius = norm(filletEdges[1] - filletEdges[0]) / 3;
    
            try(opFillet(context, id + "fillet1", {
                            "entities" : qUnion(filletEdges3D),
                            "radius" : filletRadius
                        }));
    
            // Remove sketch entities - no longer required
            opDeleteBodies(context, id + "delete", { "entities" : qCreatedBy(id + "gearSketch") });
    
            const PDSketch = newSketchOnPlane(context, id + "PDsketch", { "sketchPlane" : sketchPlane });
            skCircle(PDSketch, "PCD", {
                        "center" : center2D,
                        "radius" : definition.PCD / 2,
                        "construction" : true
                    });
            skSolve(PDSketch);
    
        }, { centerHole : true, key : true });
    
    function getArcMidPoint(center is Vector, start is Vector, end is Vector) returns Vector
    {
        // need to convert 2D vectors back to 3D
        const center3D = vector(center[0], center[1], 0 * meter);
        const start3D = vector(start[0], start[1], 0 * meter);
        const end3D = vector(end[0], end[1], 0 * meter);
    
        const angle = angleBetween(center3D - start3D, center3D - end3D) / 2;
        start = center - start;
    
        var ca = cos(angle); // in radians
        var sa = sin(angle);
        return center - vector(ca * start[0] - sa * start[1], sa * start[0] + ca * start[1]);
    }
    
    function toWorldVector(csys is CoordSystem, point is Vector, depth is map, direction is boolean) returns Vector
    {
        var dir = direction ? -1 : 1;
        var vector3D = vector(point[0], point[1], dir * depth / 2);
        return toWorld(csys, vector3D);
    }
    
    export function editGearLogic(context is Context, id is Id, oldDefinition is map, definition is map,
        isCreating is boolean, specifieCParameters is map, hiddenBodies is Query) returns map
    {
        if (oldDefinition.numTeeth != definition.numTeeth)
        {
            definition.MD = definition.PCD / definition.numTeeth;
            definition.CP = definition.MD * PI;
            definition.DP = 1 * inch / definition.MD;
            return definition;
        }
        if (oldDefinition.CP != definition.CP)
        {
            definition.MD = definition.CP / PI;
            definition.PCD = (definition.CP * definition.numTeeth) / PI;
            definition.DP = 1 * inch / definition.MD;
            return definition;
        }
        if (oldDefinition.PCD != definition.PCD)
        {
            definition.MD = definition.PCD / definition.numTeeth;
            definition.CP = (PI * definition.PCD) / definition.numTeeth;
            definition.DP = 1 * inch / definition.MD;
            return definition;
        }
        if (oldDefinition.MD != definition.MD)
        {
            definition.CP = definition.MD * PI;
            definition.PCD = definition.numTeeth * definition.MD;
            definition.DP = 1 * inch / definition.MD;
            return definition;
        }
        if (oldDefinition.DP != definition.DP)
        {
            definition.CP = PI / (definition.DP / inch);
            definition.MD = definition.CP / PI;
            definition.PCD = (definition.CP * definition.numTeeth) / PI;
            return definition;
        }
        return definition;
    }
    
    const TEETH_BOUNDS =
    {
                "min" : 4,
                "max" : 1e9,
                (unitless) : [4, 25, 1e5]
            } as IntegerBoundSpec;
    
    const PRESSURE_ANGLE_BOUNDS =
    {
                "min" : 12 * degree,
                "max" : 35 * degree,
                (degree) : [12, 20, 35]
            } as AngleBoundSpec;
    
    const OFFSET_ANGLE_BOUNDS =
    {
                "min" : -TOLERANCE.zeroAngle * radian,
                "max" : (2 * PI + TOLERANCE.zeroAngle) * radian,
                (degree) : [0, 0, 360],
                (radian) : 0
            } as AngleBoundSpec;
    
    const MODULE_BOUNDS =
    {
                "min" : -TOLERANCE.zeroLength * meter,
                "max" : 500 * meter,
                (meter) : [1e-5, 0.001, 500],
                (centimeter) : 0.1,
                (millimeter) : 1.0,
                (inch) : 0.04
            } as LengthBoundSpec;
    
    const CENTERHOLE_BOUNDS =
    {
                "min" : -TOLERANCE.zeroLength * meter,
                "max" : 500 * meter,
                (meter) : [1e-5, 0.006, 500],
                (centimeter) : 0.6,
                (millimeter) : 6.0,
                (inch) : 0.25
            } as LengthBoundSpec;
    
    const KEY_WIDTH_BOUNDS =
    {
                "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 KEY_HEIGHT_BOUNDS =
    {
                "min" : -TOLERANCE.zeroLength * meter,
                "max" : 500 * meter,
                (meter) : [1e-5, 0.008, 500],
                (centimeter) : 0.8,
                (millimeter) : 8.0,
                (inch) : 0.32
            } as LengthBoundSpec;
    
    export enum GearInputType
    {
        annotation { "Name" : "Module" }
        MD,
        annotation { "Name" : "Diametral pitch" }
        DP,
        annotation { "Name" : "Circular pitch" }
        CP
    }
    
    Neil Cooke, Director of Technical Marketing, Onshape Inc.
  • fastwayjimfastwayjim Member, OS Professional, Mentor Posts: 218 PRO
    Nice spur gear! I was just thinking about this... And now seeing how much code you did, I'm glad I didn't even try it... Seriously though, a clocking keyway?!?!
  • joe_dunnejoe_dunne Onshape Employees, Developers Posts: 146
    Hey Ilya,

    You don't by any chance have an example as to the format for the rect array csv file?
    Joe Dunne / Onshape, Inc.
  • ilya_baranilya_baran Onshape Employees, Developers, HDM Posts: 915
    That's the nice thing about the CSV format -- it's just a bunch of numbers separated by commas (and newlines):

    1,2,3
    0.1,0.5,3.5
    -1,2,2
    Ilya Baran \ Director of FeatureScript \ Onshape Inc
  • fastwayjimfastwayjim Member, OS Professional, Mentor Posts: 218 PRO
    Okay, I'm loving the spur gear FS , but the pitch diameter sketches will not hide. How can we fix this?


  • jakeramsleyjakeramsley Member, Moderator, Onshape Employees, Developers Posts: 565
    edited July 2016
    Okay, I'm loving the spur gear FS , but the pitch diameter sketches will not hide. How can we fix this?



    In the feature list there should be an eye controlling the visibility of the sketch.  Graphically, you can select it and press 'y'.

    edit:

    If you are talking programmatically, follow what Neil does with the other sketches:
    // Remove sketch entities - no longer required
            opDeleteBodies(context, id + "delete", { "entities" : qCreatedBy(id + "gearSketch") });
    
    and add in the sketch for the pitch diameter (or simply remove the pitch diameter sketch code).
    skCircle(PDSketch, "PCD", {
                        "center" : center2D,
                        "radius" : definition.PCD / 2,
                        "construction" : true
                    });
    Jake Ramsley

    Director of Quality Engineering              onshape.com
  • fastwayjimfastwayjim Member, OS Professional, Mentor Posts: 218 PRO
    Ah! You have to hide the feature... duh. Thanks!
  • sebastian_glanznersebastian_glanzner Member Posts: 96 PRO
    NeilCooke said:
    Wave Spring example:
    Hi Neil, your Wave Spring feature saved me a lot of work!
    Very useful, thanks!
Sign In or Register to comment.