Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape.
First time visiting? Here are some places to start:- Looking for a certain topic? Check out the categories filter or use Search (upper right).
- Need support? Ask a question to our Community Support category.
- Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
- 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_baran
Onshape Employees, Developers, HDM Posts: 1,245
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.
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 \ VP, Architecture and FeatureScript \ Onshape Inc
1
Comments
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 });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) }); });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 });
Code:
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 */ });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 }); } }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 }); } }
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 }
Linked[in]
You don't by any chance have an example as to the format for the rect array csv file?
Below is the code for a feature that will generate a hexagonal mesh in the interior of apart.
This makes the part both lighter, quicker to 3D print and uses less material.
The feature takes as input;
The part
The shell thickness
The hex size.
The preview option lets you see a quick preview of the hex layout. Unchecking preview actually generates the geometry.
export const INFILL_BOUNDS = { "min" : 1, "max" : 100, (unitless) : [1, 10, 100] } as IntegerBoundSpec; export const HEX_BOUNDS = { "min" : 1 * millimeter, "max" : 100 * millimeter, (meter) : [0.0, 0.025, 500], (centimeter) : 0.5, (millimeter) : [1, 5, 100], (inch) : .25, (foot) : 0.015, (yard) : 0.005 } as LengthBoundSpec; annotation { "Feature Type Name" : "HexInfill" } export const myFeature = defineFeature(function(context is Context, id is Id, definition is map) precondition { annotation { "Name" : "Part To Shell", "Filter" : EntityType.BODY, "MaxNumberOfPicks" : 1 } definition.PartToShell is Query; annotation { "Name" : "Shell Thickness" } isLength(definition.ShellThickness, HEX_BOUNDS); annotation { "Name" : "Hex Width" } isLength(definition.HexWidth, HEX_BOUNDS); annotation { "Name" : "% InFill" } isInteger(definition.InFill, INFILL_BOUNDS); annotation { "Name" : "HexPreview", "Default" : true } definition.HexPreview is boolean; } //----------------- { var boundingBox is Box3d = evBox3d(context, { "topology" : definition.PartToShell }); println("Begin Processing -----------"); var bbx1 = boundingBox.minCorner[0]; var bby1 = boundingBox.minCorner[1]; var bbz1 = boundingBox.minCorner[2]; var bbx2 = boundingBox.maxCorner[0]; var bby2 = boundingBox.maxCorner[1]; var bbz2 = boundingBox.maxCorner[2]; var FullWidthDimless = (definition.HexWidth) / meter; var HalfWidthDimless = (definition.HexWidth / 2) / meter; var LongArm = HalfWidthDimless / cos(30 * degree); var Flat = (HalfWidthDimless * tan(30 * degree)) * 2; var Yincrement = LongArm + (Flat / 2); var TotalXcells = ceil((bbx2 - bbx1) / definition.HexWidth); var TotalYcells = ceil((bby2 - bby1) / (Yincrement * meter)) + 1; var TotalCellCount = TotalXcells * TotalYcells; println("Total Cells = " ~ TotalCellCount); var AreaOfHex = (6 * (HalfWidthDimless * HalfWidthDimless)) / (sqrt(3)); var ReducedArea = AreaOfHex * ((100 - definition.InFill) / 100); var ReducedHalfWidth = sqrt((sqrt(3) * ReducedArea) / 6); var ThickenAmount = (FullWidthDimless - (ReducedHalfWidth * 2)) / 2; println(AreaOfHex); println(ThickenAmount); var Origin = vector([0, 0, bbz1]) * meter; // lower z bound var Normal = vector([0, 0, 1]); // straight up var Xdir = vector([1, 0, 0]); // define x direction in the new sketch // create plane at lower z bound var HexPlane is Plane = plane(Origin, Normal, Xdir); // create sketch var HexSketch = newSketchOnPlane(context, id + "HexSketch", { "sketchPlane" : HexPlane }); var CurrentCell = 0; var x; // cell index x var y; // cell index y var x1 = makeArray(6); var y1 = makeArray(6); var x2 = makeArray(6); var y2 = makeArray(6); for (var i = 0; i < 6; i += 1) { var ang = i * 60; x1[i] = LongArm * cos((ang - 30) * degree); y1[i] = LongArm * sin((ang - 30) * degree); x2[i] = LongArm * cos((ang + 30) * degree); y2[i] = LongArm * sin((ang + 30) * degree); } print("Starting main loop "); for (y = 0; y < TotalYcells; y += 1) { var Xcenter = (bbx1 - definition.HexWidth) / meter; var Ycenter = (bby1 + (y * (Yincrement * meter))) / meter; if (y % 2 != 0) // odd Xcenter += (FullWidthDimless / 2); for (x = 0; x < TotalXcells; x += 1) { Xcenter += FullWidthDimless; for (var i = 0; i < 6; i += 1) { var LineStart = vector([Xcenter + x1[i], Ycenter + y1[i]]) * meter; var LineEnd__ = vector([Xcenter + x2[i], Ycenter + y2[i]]) * meter; var LineID = "line" ~ CurrentCell ~ "-" ~ i; skLineSegment(HexSketch, LineID, { "start" : LineStart, "end" : LineEnd__ }); print("."); } CurrentCell += 1; } } skSolve(HexSketch); if (!definition.HexPreview) { var SixEdges = makeArray(6); print("\nExtrude-Thicken-Delete "); for (var i = 0; i < TotalCellCount; i += 1) { for (var j = 0; j < 6; j += 1) SixEdges[j] = sketchEntityQuery(id + "HexSketch", EntityType.EDGE, "line" ~ i ~ "-" ~ j); var SixEdgeQuery = qUnion(SixEdges); extrude(context, id + ("Extrude" ~ i), { "surfaceEntities" : SixEdgeQuery, "bodyType" : ToolBodyType.SURFACE, "endBound" : BoundingType.BLIND, "depth" : bbz2 - bbz1, 'operationtype' : NewBodyOperationType.NEW }); var SurfaceCreated = qCreatedBy(id + ("Extrude" ~ i), EntityType.FACE); thicken(context, id + ("Thicken" ~ i), { "operationType" : NewBodyOperationType.NEW, "entities" : SurfaceCreated, "thickness1" : ThickenAmount * meter, "oppositeDirection" : false, "thickness2" : ThickenAmount * meter, "defaultScope" : false, "booleanScope" : qUnion([]) }); opDeleteBodies(context, id + ("DeleteSurface" ~ i), { "entities" : SurfaceCreated }); print("."); } print("\nBoolean Hexes Together "); var HexParts = makeArray(TotalCellCount); for (var i = 0; i < TotalCellCount; i += 1) { HexParts[i] = qCreatedBy(id + ("Thicken" ~ i), EntityType.BODY); print("."); } booleanBodies(context, id + "BooleanHexes", { "tools" : qUnion(HexParts), "targets" : qUnion([]), "operationType" : BooleanOperationType.UNION }); print("\nMake a copy of the original part "); transform(context, id + "MakeCopy", { "entities" : definition.PartToShell, "transformType" : TransformType.COPY, "makeCopy" : true }); print("\nBoolean Intersection of Copy and Hexes "); var CopyAndHexes = qUnion([qCreatedBy(id + "MakeCopy", EntityType.BODY), qCreatedBy(id + "Thicken0", EntityType.BODY)]); opBoolean(context, id + "BooleanIntersection", { "tools" : CopyAndHexes, "operationType" : BooleanOperationType.INTERSECTION }); print("\nShelling the original part "); shell(context, id + "shell", { "parts" : definition.PartToShell, "thickness" : definition.ShellThickness, "isHollow" : true }); print("\nBoolean Union of shelled part and Hexes "); var PartAndHex = qUnion([qCreatedBy(id + "BooleanIntersection", EntityType.BODY), definition.PartToShell]); opBoolean(context, id + "BooleanUnion", { "tools" : PartAndHex, "operationType" : BooleanOperationType.UNION }); print("\nDeleteing Sketch Entities\n\n"); var StuffInSketch = qCreatedBy(id + "HexSketch", EntityType.BODY); opDeleteBodies(context, id + "DeleteSketchContents", { "entities" : StuffInSketch }); } // endif HexPreview }, { /* default parameters */ });
Linked[in]
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 });Linked[in]
Very useful, thanks!